Skip to main content

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
35const DEFAULT_MEMORY_SIZE: u64 = 1024 * 1024 * 1024;
36
37/// Guest memory configuration parsed from `--memory`.
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct MemoryCli {
40    /// Guest RAM size in bytes.
41    pub mem_size: u64,
42    /// Whether shared file-backed memory was explicitly requested.
43    pub shared: Option<bool>,
44    /// Whether to prefetch guest RAM.
45    pub prefetch: bool,
46    /// Whether to use transparent huge pages for private guest RAM.
47    pub transparent_hugepages: bool,
48    /// Whether to use explicit hugetlb memfd backing for guest RAM.
49    pub hugepages: bool,
50    /// Explicit hugetlb page size in bytes.
51    pub hugepage_size: Option<u64>,
52    /// File used to back guest RAM.
53    pub file: Option<PathBuf>,
54}
55
56/// OpenVMM virtual machine monitor.
57///
58/// This is not yet a stable interface and may change radically between
59/// versions.
60#[derive(Parser)]
61pub struct Options {
62    /// processor count
63    #[clap(short = 'p', long, value_name = "COUNT", default_value = "1")]
64    pub processors: u32,
65
66    /// guest RAM configuration (`SIZE` or `key=value[,key=value...]`)
67    #[clap(
68        short = 'm',
69        long,
70        value_name = "PARAMS",
71        default_value = "1GB",
72        value_parser = parse_memory_config,
73        conflicts_with = "numa_memory",
74        long_help = r#"Configure guest RAM.
75
76Syntax: SIZE | key=value[,key=value...]
77
78Size suffixes accept K, M, G, and T, optionally followed by B.
79
80Options:
81    size=<SIZE>              guest RAM size, default 1GB
82    shared=on|off            use shared file-backed RAM, default on
83    prefetch=on|off          pre-populate shared RAM mappings
84    thp=on|off               mark private RAM as THP-eligible; requires shared=off
85    hugepages=on|off         allocate RAM from Linux hugetlb pages
86    hugepage_size=<SIZE>     hugetlb page size, default 2MB; requires hugepages=on
87    file=<PATH>              use an existing file as guest RAM backing
88
89Examples:
90    --memory 4G
91    --memory size=64GB,hugepages=on,hugepage_size=2MB
92    --memory size=4G,file=path/to/memory.bin
93    --memory size=4G,shared=off,thp=on"#
94    )]
95    pub memory: MemoryCli,
96
97    /// per-NUMA-node guest RAM sizes (comma-separated, e.g. "2G,2G").
98    /// Distributes memory across vNUMA nodes reported to the guest. Mutually
99    /// exclusive with --memory. This is for test-only usage.
100    ///
101    /// TODO: Backing pages are not pinned to any host topology, nor coordinated
102    /// with CPUs. This should change once we implement real numa support.
103    #[clap(long, value_name = "SIZES", value_parser = parse_memory, value_delimiter = ',', conflicts_with = "memory")]
104    pub numa_memory: Option<Vec<u64>>,
105
106    /// use shared memory segment
107    #[clap(short = 'M', long, hide = true)]
108    pub shared_memory: bool,
109
110    /// prefetch guest RAM
111    #[clap(long = "prefetch", hide = true)]
112    pub deprecated_prefetch: bool,
113
114    /// back guest RAM with a file instead of anonymous memory.
115    /// The file is created/opened and sized to the guest RAM size.
116    /// Enables snapshot save (fsync) and restore (open + mmap).
117    #[clap(
118        long = "memory-backing-file",
119        value_name = "FILE",
120        hide = true,
121        conflicts_with = "deprecated_private_memory"
122    )]
123    pub deprecated_memory_backing_file: Option<PathBuf>,
124
125    /// Restore VM from a snapshot directory (implies file-backed memory from
126    /// the snapshot's memory.bin). Cannot be used with --memory-backing-file.
127    #[clap(
128        long,
129        value_name = "DIR",
130        conflicts_with = "deprecated_memory_backing_file"
131    )]
132    pub restore_snapshot: Option<PathBuf>,
133
134    /// use private anonymous memory for guest RAM
135    #[clap(long = "private-memory", hide = true, conflicts_with_all = ["deprecated_memory_backing_file", "restore_snapshot"])]
136    pub deprecated_private_memory: bool,
137
138    /// enable transparent huge pages for guest RAM (Linux only, requires --private-memory)
139    #[clap(long = "thp", hide = true)]
140    pub deprecated_thp: bool,
141
142    /// start in paused state
143    #[clap(short = 'P', long)]
144    pub paused: bool,
145
146    /// kernel image (when using linux direct boot)
147    #[clap(short = 'k', long, value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_LINUX_DIRECT_KERNEL"))]
148    pub kernel: OptionalPathBuf,
149
150    /// initrd image (when using linux direct boot)
151    #[clap(short = 'r', long, value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_LINUX_DIRECT_INITRD"))]
152    pub initrd: OptionalPathBuf,
153
154    /// extra kernel command line args
155    #[clap(short = 'c', long, value_name = "STRING")]
156    pub cmdline: Vec<String>,
157
158    /// enable HV#1 capabilities
159    #[clap(long)]
160    pub hv: bool,
161
162    /// Use a full device tree instead of ACPI tables for ARM64 Linux direct
163    /// boot. By default, ARM64 uses ACPI mode (stub DT + EFI + ACPI tables).
164    /// This flag selects the legacy DT-only path. Rejected on x86.
165    #[clap(long, conflicts_with_all = ["uefi", "pcat", "igvm"])]
166    pub device_tree: bool,
167
168    /// enable vtl2 - only supported in WHP and simulated without hypervisor support currently
169    ///
170    /// Currently implies --get.
171    #[clap(long, requires("hv"))]
172    pub vtl2: bool,
173
174    /// Add GET and related devices for using the OpenHCL paravisor to the
175    /// highest enabled VTL.
176    #[clap(long, requires("hv"))]
177    pub get: bool,
178
179    /// Disable GET and related devices for using the OpenHCL paravisor, even
180    /// when --vtl2 is passed.
181    #[clap(long, conflicts_with("get"))]
182    pub no_get: bool,
183
184    /// disable the VTL0 alias map presented to VTL2 by default
185    #[clap(long, requires("vtl2"))]
186    pub no_alias_map: bool,
187
188    /// enable isolation emulation
189    #[clap(long, requires("vtl2"))]
190    pub isolation: Option<IsolationCli>,
191
192    /// the hybrid vsock listener path
193    #[clap(long, value_name = "PATH", alias = "vsock-path")]
194    pub vmbus_vsock_path: Option<String>,
195
196    /// the VTL2 hybrid vsock listener path
197    #[clap(long, value_name = "PATH", requires("vtl2"), alias = "vtl2-vsock-path")]
198    pub vmbus_vtl2_vsock_path: Option<String>,
199
200    /// the late map vtl0 ram access policy when vtl2 is enabled
201    #[clap(long, requires("vtl2"), default_value = "halt")]
202    pub late_map_vtl0_policy: Vtl0LateMapPolicyCli,
203
204    /// attach a disk (can be passed multiple times)
205    #[clap(long_help = r#"
206e.g: --disk memdiff:file:/path/to/disk.vhd
207
208syntax: <path> | kind:<arg>[,flag,opt=arg,...]
209
210valid disk kinds:
211    `mem:<len>`                    memory backed disk
212        <len>: length of ramdisk, e.g.: `1G`
213    `memdiff:<disk>`               memory backed diff disk
214        <disk>: lower disk, e.g.: `file:base.img`
215    `file:<path>[;direct][;create=<len>]`   file-backed disk
216        <path>: path to file
217        `;direct`: bypass the OS page cache
218    `sql:<path>[;create=<len>]`    SQLite-backed disk (dev/test)
219    `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
220    `autocache:<key>:<disk>`       auto-cached SQLite layer (use `autocache::<disk>` to omit key; needs OPENVMM_AUTO_CACHE_PATH)
221    `blob:<type>:<url>`            HTTP blob (read-only)
222        <type>: `flat` or `vhd1`
223    `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
224        <cipher>: `xts-aes-256`
225    `prwrap:<disk>`                persistent reservations wrapper
226
227flags:
228    `ro`                           open disk as read-only
229    `dvd`                          specifies that device is cd/dvd and it is read_only
230    `vtl2`                         assign this disk to VTL2
231    `uh`                           relay this disk to VTL0 through SCSI-to-OpenHCL (show to VTL0 as SCSI)
232    `uh-nvme`                      relay this disk to VTL0 through NVMe-to-OpenHCL (show to VTL0 as SCSI)
233
234options:
235    `pcie_port=<name>`             present the disk using pcie under the specified port, incompatible with `dvd`, `vtl2`, `uh`, and `uh-nvme`
236"#)]
237    #[clap(long, value_name = "FILE")]
238    pub disk: Vec<DiskCli>,
239
240    /// attach a disk via an NVMe controller
241    #[clap(long_help = r#"
242e.g: --nvme memdiff:file:/path/to/disk.vhd
243
244syntax: <path> | kind:<arg>[,flag,opt=arg,...]
245
246valid disk kinds:
247    `mem:<len>`                    memory backed disk
248        <len>: length of ramdisk, e.g.: `1G`
249    `memdiff:<disk>`               memory backed diff disk
250        <disk>: lower disk, e.g.: `file:base.img`
251    `file:<path>[;direct][;create=<len>]`   file-backed disk
252        <path>: path to file
253        `;direct`: bypass the OS page cache
254    `sql:<path>[;create=<len>]`    SQLite-backed disk (dev/test)
255    `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
256    `autocache:<key>:<disk>`       auto-cached SQLite layer (use `autocache::<disk>` to omit key; needs OPENVMM_AUTO_CACHE_PATH)
257    `blob:<type>:<url>`            HTTP blob (read-only)
258        <type>: `flat` or `vhd1`
259    `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
260        <cipher>: `xts-aes-256`
261    `prwrap:<disk>`                persistent reservations wrapper
262
263flags:
264    `ro`                           open disk as read-only
265    `vtl2`                         assign this disk to VTL2
266    `uh`                           relay this disk to VTL0 through SCSI-to-OpenHCL (show to VTL0 as NVMe)
267    `uh-nvme`                      relay this disk to VTL0 through NVMe-to-OpenHCL (show to VTL0 as NVMe)
268
269options:
270    `pcie_port=<name>`             present the disk using pcie under the specified port, incompatible with `vtl2`, `uh`, and `uh-nvme`
271"#)]
272    #[clap(long)]
273    pub nvme: Vec<DiskCli>,
274
275    /// attach a disk via a virtio-blk controller
276    #[clap(long_help = r#"
277e.g: --virtio-blk memdiff:file:/path/to/disk.vhd
278
279syntax: <path> | kind:<arg>[,flag,opt=arg,...]
280
281valid disk kinds:
282    `mem:<len>`                    memory backed disk
283        <len>: length of ramdisk, e.g.: `1G`
284    `memdiff:<disk>`               memory backed diff disk
285        <disk>: lower disk, e.g.: `file:base.img`
286    `file:<path>[;direct]`                  file-backed disk
287        <path>: path to file
288        `;direct`: bypass the OS page cache
289
290flags:
291    `ro`                           open disk as read-only
292
293options:
294    `pcie_port=<name>`             present the disk using pcie under the specified port
295"#)]
296    #[clap(long = "virtio-blk")]
297    pub virtio_blk: Vec<DiskCli>,
298
299    /// Attach a vhost-user device via a Unix socket.
300    ///
301    /// The first positional argument is the socket path. Options:
302    ///
303    /// ```text
304    ///   type=blk|fs                        — device type (shorthand)
305    ///   device_id=N                        — numeric virtio device ID
306    ///   tag=NAME                           — mount tag (required for type=fs)
307    ///   num_queues=N                       — queue count (type=blk/fs only)
308    ///   queue_size=N                       — per-queue size (type=blk/fs only)
309    ///   queue_sizes=[N,N,N]                — per-queue sizes (device_id= only)
310    ///   pcie_port=NAME                     — present on PCIe under the specified port
311    /// ```
312    ///
313    /// Examples:
314    ///
315    /// ```text
316    ///   --vhost-user /tmp/vhost.sock,type=blk
317    ///   --vhost-user /tmp/vhost.sock,type=blk,num_queues=4,queue_size=512
318    ///   --vhost-user /tmp/vhost.sock,device_id=2,queue_sizes=[128,128]
319    ///   --vhost-user /tmp/vhost.sock,type=blk,pcie_port=port0
320    ///   --vhost-user /tmp/virtiofsd.sock,type=fs,tag=myfs
321    ///   --vhost-user /tmp/virtiofsd.sock,type=fs,tag=myfs,num_queues=2,queue_size=1024
322    /// ```
323    #[cfg(target_os = "linux")]
324    #[clap(long = "vhost-user")]
325    pub vhost_user: Vec<VhostUserCli>,
326
327    /// number of sub-channels for the SCSI controller
328    #[clap(long, value_name = "COUNT", default_value = "0")]
329    pub scsi_sub_channels: u16,
330
331    /// expose a virtual NIC
332    #[clap(long)]
333    pub nic: bool,
334
335    /// expose a virtual NIC with the given backend (consomme | dio | tap | none)
336    ///
337    /// Prefix with `uh:` to add this NIC via Mana emulation through OpenHCL,
338    /// `vtl2:` to assign this NIC to VTL2, or `pcie_port=<port_name>:` to
339    /// expose the NIC over emulated PCIe at the specified port.
340    ///
341    /// For consomme, forward host ports into the guest with `hostfwd=`:
342    ///   --net consomme:hostfwd=tcp::3389-:3389
343    ///   --net consomme:hostfwd=tcp:127.0.0.1:8080-:80
344    ///   --net consomme:hostfwd=tcp:\[::1\]:8080-:80
345    ///   --net consomme:10.0.0.0/24,hostfwd=tcp::22-:22,hostfwd=udp::5000-:5000
346    #[clap(long)]
347    pub net: Vec<NicConfigCli>,
348
349    /// expose a virtual NIC using the Windows kernel-mode vmswitch.
350    ///
351    /// Specify the switch ID or "default" for the default switch.
352    #[clap(long, value_name = "SWITCH_ID")]
353    pub kernel_vmnic: Vec<String>,
354
355    /// expose a graphics device
356    #[clap(long)]
357    pub gfx: bool,
358
359    /// support a graphics device in vtl2
360    #[clap(long, requires("vtl2"), conflicts_with("gfx"))]
361    pub vtl2_gfx: bool,
362
363    /// listen for vnc connections. implied by gfx.
364    #[clap(long)]
365    pub vnc: bool,
366
367    /// VNC port number
368    #[clap(long, value_name = "PORT", default_value = "5900")]
369    pub vnc_port: u16,
370
371    /// set the APIC ID offset, for testing APIC IDs that don't match VP index
372    #[cfg(guest_arch = "x86_64")]
373    #[clap(long, default_value_t)]
374    pub apic_id_offset: u32,
375
376    /// the maximum number of VPs per socket
377    #[clap(long)]
378    pub vps_per_socket: Option<u32>,
379
380    /// enable or disable SMT (hyperthreading) (auto | force | off)
381    #[clap(long, default_value = "auto")]
382    pub smt: SmtConfigCli,
383
384    /// configure x2apic (auto | supported | off | on)
385    #[cfg(guest_arch = "x86_64")]
386    #[clap(long, default_value = "auto", value_parser = parse_x2apic)]
387    pub x2apic: X2ApicConfig,
388
389    /// configure PCIe MSI controller for aarch64 (auto | its | v2m)
390    #[cfg(guest_arch = "aarch64")]
391    #[clap(long, default_value = "auto")]
392    pub gic_msi: GicMsiCli,
393
394    /// COM1 binding (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>]\[,name=\<windowtitle\>\] | none)
395    #[clap(long, value_name = "SERIAL")]
396    pub com1: Option<SerialConfigCli>,
397
398    /// COM2 binding (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>]\[,name=\<windowtitle\>\] | none)
399    #[clap(long, value_name = "SERIAL")]
400    pub com2: Option<SerialConfigCli>,
401
402    /// COM3 binding (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>]\[,name=\<windowtitle\>\] | none)
403    #[clap(long, value_name = "SERIAL")]
404    pub com3: Option<SerialConfigCli>,
405
406    /// COM4 binding (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>]\[,name=\<windowtitle\>\] | none)
407    #[clap(long, value_name = "SERIAL")]
408    pub com4: Option<SerialConfigCli>,
409
410    /// vmbus com1 serial binding (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>]\[,name=\<windowtitle\>\] | none)
411    #[structopt(long, value_name = "SERIAL")]
412    pub vmbus_com1_serial: Option<SerialConfigCli>,
413
414    /// vmbus com2 serial binding (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>]\[,name=\<windowtitle\>\] | none)
415    #[structopt(long, value_name = "SERIAL")]
416    pub vmbus_com2_serial: Option<SerialConfigCli>,
417
418    /// Only allow guest to host serial traffic
419    #[clap(long)]
420    pub serial_tx_only: bool,
421
422    /// 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))
423    #[clap(long, value_name = "SERIAL")]
424    pub debugcon: Option<DebugconSerialConfigCli>,
425
426    /// boot UEFI firmware
427    #[clap(long, short = 'e')]
428    pub uefi: bool,
429
430    /// UEFI firmware file
431    #[clap(long, requires("uefi"), conflicts_with("igvm"), value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_UEFI_FIRMWARE"))]
432    pub uefi_firmware: OptionalPathBuf,
433
434    /// enable UEFI debugging on COM1
435    #[clap(long, requires("uefi"))]
436    pub uefi_debug: bool,
437
438    /// enable memory protections in UEFI
439    #[clap(long, requires("uefi"))]
440    pub uefi_enable_memory_protections: bool,
441
442    /// set PCAT boot order as comma-separated string of boot device types
443    /// (e.g: floppy,hdd,optical,net).
444    ///
445    /// If less than 4 entries are added, entries are added according to their
446    /// default boot order (optical,hdd,net,floppy)
447    ///
448    /// e.g: passing "floppy,optical" will result in a boot order equivalent to
449    /// "floppy,optical,hdd,net".
450    ///
451    /// Passing duplicate types is an error.
452    #[clap(long, requires("pcat"))]
453    pub pcat_boot_order: Option<PcatBootOrderCli>,
454
455    /// Boot with PCAT BIOS firmware and piix4 devices
456    #[clap(long, conflicts_with("uefi"))]
457    pub pcat: bool,
458
459    /// PCAT firmware file
460    #[clap(long, requires("pcat"), value_name = "FILE")]
461    pub pcat_firmware: Option<PathBuf>,
462
463    /// boot IGVM file
464    #[clap(long, conflicts_with("kernel"), value_name = "FILE")]
465    pub igvm: Option<PathBuf>,
466
467    /// specify igvm vtl2 relocation type
468    /// (absolute=\<addr\>, disable, auto=\<filesize,or memory size\>, vtl2=\<filesize,or memory size\>,)
469    #[clap(long, requires("igvm"), default_value = "auto=filesize", value_parser = parse_vtl2_relocation)]
470    pub igvm_vtl2_relocation_type: Vtl2BaseAddressType,
471
472    /// add a virtio_9p device (e.g. myfs,C:\)
473    ///
474    /// Prefix with `pcie_port=<port_name>:` to expose the device over
475    /// emulated PCIe at the specified port.
476    #[clap(long, value_name = "[pcie_port=PORT:]tag,root_path")]
477    pub virtio_9p: Vec<FsArgs>,
478
479    /// output debug info from the 9p server
480    #[clap(long)]
481    pub virtio_9p_debug: bool,
482
483    /// add a virtio_fs device (e.g. myfs,C:\,uid=1000,gid=2000)
484    ///
485    /// Prefix with `pcie_port=<port_name>:` to expose the device over
486    /// emulated PCIe at the specified port.
487    #[clap(long, value_name = "[pcie_port=PORT:]tag,root_path,[options]")]
488    pub virtio_fs: Vec<FsArgsWithOptions>,
489
490    /// add a virtio_fs device for sharing memory (e.g. myfs,\SectionDirectoryPath)
491    ///
492    /// Prefix with `pcie_port=<port_name>:` to expose the device over
493    /// emulated PCIe at the specified port.
494    #[clap(long, value_name = "[pcie_port=PORT:]tag,root_path")]
495    pub virtio_fs_shmem: Vec<FsArgs>,
496
497    /// add a virtio_fs device under either the PCI or MMIO bus, or whatever the hypervisor supports (pci | mmio | auto)
498    #[clap(long, value_name = "BUS", default_value = "auto")]
499    pub virtio_fs_bus: VirtioBusCli,
500
501    /// virtio PMEM device
502    ///
503    /// Prefix with `pcie_port=<port_name>:` to expose the device over
504    /// emulated PCIe at the specified port.
505    #[clap(long, value_name = "[pcie_port=PORT:]PATH")]
506    pub virtio_pmem: Option<VirtioPmemArgs>,
507
508    /// add a virtio entropy (RNG) device
509    #[clap(long)]
510    pub virtio_rng: bool,
511
512    /// add a virtio-rng device under either the PCI or MMIO bus, or whatever the hypervisor supports (pci | mmio | vpci | auto)
513    #[clap(long, value_name = "BUS", default_value = "auto")]
514    pub virtio_rng_bus: VirtioBusCli,
515
516    /// attach the virtio-rng device to the specified PCIe port (overrides --virtio-rng-bus)
517    #[clap(long, value_name = "PORT", requires("virtio_rng"))]
518    pub virtio_rng_pcie_port: Option<String>,
519
520    /// virtio console device backed by a serial backend (/dev/hvc0 in guest)
521    ///
522    /// Accepts serial config (console | stderr | listen=\<path\> |
523    /// file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> |
524    /// term[=\<program\>]\[,name=\<windowtitle\>\] | none)
525    #[clap(long)]
526    pub virtio_console: Option<SerialConfigCli>,
527
528    /// attach the virtio-console device to the specified PCIe port
529    #[clap(long, value_name = "PORT", requires("virtio_console"))]
530    pub virtio_console_pcie_port: Option<String>,
531
532    /// add a virtio vsock device with the given Unix socket base path
533    #[clap(long, value_name = "PATH")]
534    pub virtio_vsock_path: Option<String>,
535
536    /// expose a virtio network with the given backend (dio | vmnic | tap |
537    /// none)
538    ///
539    /// Prefix with `uh:` to add this NIC via Mana emulation through OpenHCL,
540    /// `vtl2:` to assign this NIC to VTL2, or `pcie_port=<port_name>:` to
541    /// expose the NIC over emulated PCIe at the specified port.
542    #[clap(long)]
543    pub virtio_net: Vec<NicConfigCli>,
544
545    /// send log output from the worker process to a file instead of stderr. the file will be overwritten.
546    #[clap(long, value_name = "PATH")]
547    pub log_file: Option<PathBuf>,
548
549    /// write the process ID to the specified file on startup, and remove it on
550    /// exit. the file is not removed if the process is killed with SIGKILL or
551    /// crashes. no file locking is performed.
552    #[clap(long, value_name = "PATH")]
553    pub pidfile: Option<PathBuf>,
554
555    /// run as a ttrpc server on the specified Unix socket
556    #[clap(long, value_name = "SOCKETPATH")]
557    pub ttrpc: Option<PathBuf>,
558
559    /// run as a grpc server on the specified Unix socket
560    #[clap(long, value_name = "SOCKETPATH", conflicts_with("ttrpc"))]
561    pub grpc: Option<PathBuf>,
562
563    /// do not launch child processes
564    #[clap(long)]
565    pub single_process: bool,
566
567    /// device to assign (can be passed multiple times)
568    #[cfg(windows)]
569    #[clap(long, value_name = "PATH")]
570    pub device: Vec<String>,
571
572    /// instead of showing the frontpage the VM will shutdown instead
573    #[clap(long, requires("uefi"))]
574    pub disable_frontpage: bool,
575
576    /// add a vtpm device
577    #[clap(long)]
578    pub tpm: bool,
579
580    /// the mesh worker host name.
581    ///
582    /// Used internally for debugging and diagnostics.
583    #[clap(long, default_value = "control", hide(true))]
584    #[expect(clippy::option_option)]
585    pub internal_worker: Option<Option<String>>,
586
587    /// redirect the VTL 0 vmbus control plane to a proxy in VTL 2.
588    #[clap(long, requires("vtl2"))]
589    pub vmbus_redirect: bool,
590
591    /// limit the maximum protocol version allowed by vmbus; used for testing purposes
592    #[clap(long, value_parser = vmbus_core::parse_vmbus_version)]
593    pub vmbus_max_version: Option<u32>,
594
595    /// The disk to use for the VMGS.
596    ///
597    /// If this is not provided, guest state will be stored in memory.
598    #[clap(long_help = r#"
599e.g: --vmgs memdiff:file:/path/to/file.vmgs
600
601syntax: <path> | kind:<arg>[,flag]
602
603valid disk kinds:
604    `mem:<len>`                     memory backed disk
605        <len>: length of ramdisk, e.g.: `1G` or `VMGS_DEFAULT`
606    `memdiff:<disk>[;create=<len>]` memory backed diff disk
607        <disk>: lower disk, e.g.: `file:base.img`
608    `file:<path>`                   file-backed disk
609        <path>: path to file
610
611flags:
612    `fmt`                           reprovision the VMGS before boot
613    `fmt-on-fail`                   reprovision the VMGS before boot if it is corrupted
614"#)]
615    #[clap(long)]
616    pub vmgs: Option<VmgsCli>,
617
618    /// Use GspById guest state encryption policy with a test seed
619    #[clap(long, requires("vmgs"))]
620    pub test_gsp_by_id: bool,
621
622    /// VGA firmware file
623    #[clap(long, requires("pcat"), value_name = "FILE")]
624    pub vga_firmware: Option<PathBuf>,
625
626    /// enable secure boot
627    #[clap(long)]
628    pub secure_boot: bool,
629
630    /// use secure boot template
631    #[clap(long)]
632    pub secure_boot_template: Option<SecureBootTemplateCli>,
633
634    /// custom uefi nvram json file
635    #[clap(long, value_name = "PATH")]
636    pub custom_uefi_json: Option<PathBuf>,
637
638    /// the path to a named pipe (Windows) or Unix socket (Linux) to relay to the connected
639    /// tty.
640    ///
641    /// This is a hidden argument used internally.
642    #[clap(long, hide(true))]
643    pub relay_console_path: Option<PathBuf>,
644
645    /// the title of the console window spawned from the relay console.
646    ///
647    /// This is a hidden argument used internally.
648    #[clap(long, hide(true))]
649    pub relay_console_title: Option<String>,
650
651    /// enable in-hypervisor gdb debugger
652    #[clap(long, value_name = "PORT")]
653    pub gdb: Option<u16>,
654
655    /// enable emulated MANA devices with the given network backend (see --net)
656    ///
657    /// Prefix with `pcie_port=<port_name>:` to expose the nic over emulated PCIe
658    /// at the specified port.
659    #[clap(long)]
660    pub mana: Vec<NicConfigCli>,
661
662    /// use a specific hypervisor interface, with optional backend-specific
663    /// parameters.
664    ///
665    /// Format: `name` or `name:key=val,key,...`
666    ///
667    /// WHP parameters (x86_64 guests only):
668    ///   user_mode_apic       - use user-mode APIC emulator
669    ///   no_enlightenments    - disable in-hypervisor enlightenments
670    ///
671    /// Examples:
672    ///   --hypervisor whp
673    ///   --hypervisor whp:user_mode_apic
674    ///   --hypervisor whp:user_mode_apic,no_enlightenments
675    #[clap(long)]
676    pub hypervisor: Option<String>,
677
678    /// (dev utility) boot linux using a custom (raw) DSDT table.
679    ///
680    /// This is a _very_ niche utility, and it's unlikely you'll need to use it.
681    ///
682    /// e.g: this flag helped bring up certain Hyper-V Generation 1 legacy
683    /// devices without needing to port the associated ACPI code into OpenVMM's
684    /// DSDT builder.
685    #[clap(long, value_name = "FILE", conflicts_with_all(&["uefi", "pcat", "igvm"]))]
686    pub custom_dsdt: Option<PathBuf>,
687
688    /// attach an ide drive (can be passed multiple times)
689    ///
690    /// Each ide controller has two channels. Each channel can have up to two
691    /// attachments.
692    ///
693    /// If the `s` flag is not passed then the drive will we be attached to the
694    /// primary ide channel if space is available. If two attachments have already
695    /// been added to the primary channel then the drive will be attached to the
696    /// secondary channel.
697    #[clap(long_help = r#"
698e.g: --ide memdiff:file:/path/to/disk.vhd
699
700syntax: <path> | kind:<arg>[,flag,opt=arg,...]
701
702valid disk kinds:
703    `mem:<len>`                    memory backed disk
704        <len>: length of ramdisk, e.g.: `1G`
705    `memdiff:<disk>`               memory backed diff disk
706        <disk>: lower disk, e.g.: `file:base.img`
707    `file:<path>[;create=<len>]`   file-backed disk
708        <path>: path to file
709    `sql:<path>[;create=<len>]`    SQLite-backed disk (dev/test)
710    `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
711    `blob:<type>:<url>`            HTTP blob (read-only)
712        <type>: `flat` or `vhd1`
713    `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
714        <cipher>: `xts-aes-256`
715
716additional wrapper kinds (e.g., `autocache`, `prwrap`) are also supported;
717this list is not exhaustive.
718
719flags:
720    `ro`                           open disk as read-only
721    `s`                            attach drive to secondary ide channel
722    `dvd`                          specifies that device is cd/dvd and it is read_only
723"#)]
724    #[clap(long, value_name = "FILE", requires("pcat"))]
725    pub ide: Vec<IdeDiskCli>,
726
727    /// attach a floppy drive (should be able to be passed multiple times). VM must be generation 1 (no UEFI)
728    ///
729    #[clap(long_help = r#"
730e.g: --floppy memdiff:file:/path/to/disk.vfd,ro
731
732syntax: <path> | kind:<arg>[,flag,opt=arg,...]
733
734valid disk kinds:
735    `mem:<len>`                    memory backed disk
736        <len>: length of ramdisk, e.g.: `1G`
737    `memdiff:<disk>`               memory backed diff disk
738        <disk>: lower disk, e.g.: `file:base.img`
739    `file:<path>[;create=<len>]`   file-backed disk
740        <path>: path to file
741    `sql:<path>[;create=<len>]`    SQLite-backed disk (dev/test)
742    `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
743    `blob:<type>:<url>`            HTTP blob (read-only)
744        <type>: `flat` or `vhd1`
745    `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
746        <cipher>: `xts-aes-256`
747
748flags:
749    `ro`                           open disk as read-only
750"#)]
751    #[clap(long, value_name = "FILE", requires("pcat"))]
752    pub floppy: Vec<FloppyDiskCli>,
753
754    /// enable guest watchdog device
755    #[clap(long)]
756    pub guest_watchdog: bool,
757
758    /// enable OpenHCL's guest crash dump device, targeting the specified path
759    #[clap(long)]
760    pub openhcl_dump_path: Option<PathBuf>,
761
762    /// halt the VM when the guest requests a reset, instead of resetting it
763    #[clap(long)]
764    pub halt_on_reset: bool,
765
766    /// write saved state .proto files to the specified path
767    #[clap(long)]
768    pub write_saved_state_proto: Option<PathBuf>,
769
770    /// specify the IMC hive file for booting Windows
771    #[clap(long)]
772    pub imc: Option<PathBuf>,
773
774    /// expose a battery device
775    #[clap(long)]
776    pub battery: bool,
777
778    /// set the uefi console mode
779    #[clap(long)]
780    pub uefi_console_mode: Option<UefiConsoleModeCli>,
781
782    /// set the EFI diagnostics log level
783    #[clap(long_help = r#"
784Set the EFI diagnostics log level.
785
786options:
787    default                        default (ERROR and WARN only)
788    info                           info (ERROR, WARN, and INFO)
789    full                           full (all log levels)
790"#)]
791    #[clap(long, requires("uefi"))]
792    pub efi_diagnostics_log_level: Option<EfiDiagnosticsLogLevelCli>,
793
794    /// Perform a default boot even if boot entries exist and fail
795    #[clap(long)]
796    pub default_boot_always_attempt: bool,
797
798    /// Attach a PCI Express root complex to the VM
799    #[clap(long_help = r#"
800Attach root complexes to the VM.
801
802Examples:
803    # Attach root complex rc0 on segment 0 with bus and MMIO ranges
804    --pcie-root-complex rc0,segment=0,start_bus=0,end_bus=255,low_mmio=4M,high_mmio=1G
805
806Syntax: <name>[,opt=arg,...]
807
808Options:
809    `segment=<value>`              configures the PCI Express segment, default 0
810    `start_bus=<value>`            lowest valid bus number, default 0
811    `end_bus=<value>`              highest valid bus number, default 255
812    `low_mmio=<size>`              low MMIO window size, default 64M
813    `high_mmio=<size>`             high MMIO window size, default 1G
814"#)]
815    #[clap(long, conflicts_with("pcat"))]
816    pub pcie_root_complex: Vec<PcieRootComplexCli>,
817
818    /// Attach a PCI Express root port to the VM
819    #[clap(long_help = r#"
820Attach root ports to root complexes.
821
822Examples:
823    # Attach root port rc0rp0 to root complex rc0
824    --pcie-root-port rc0:rc0rp0
825
826    # Attach root port rc0rp1 to root complex rc0 with hotplug support
827    --pcie-root-port rc0:rc0rp1,hotplug
828
829Syntax: <root_complex_name>:<name>[,opt,opt=arg,...]
830
831Options:
832    `hotplug`                      enable hotplug support for this root port
833    `acs=<mask>`                   ACS capability bitmask (u16, decimal or 0x-prefixed hex)
834"#)]
835    #[clap(long, conflicts_with("pcat"))]
836    pub pcie_root_port: Vec<PcieRootPortCli>,
837
838    /// Attach a PCI Express switch to the VM
839    #[clap(long_help = r#"
840Attach switches to root ports or downstream switch ports to create PCIe hierarchies.
841
842Examples:
843    # Connect switch0 (with 4 downstream switch ports) directly to root port rp0
844    --pcie-switch rp0:switch0,num_downstream_ports=4
845
846    # Connect switch1 (with 2 downstream switch ports) to downstream port 0 of switch0
847    --pcie-switch switch0-downstream-0:switch1,num_downstream_ports=2
848
849    # Create a 3-level hierarchy: rp0 -> switch0 -> switch1 -> switch2
850    --pcie-switch rp0:switch0
851    --pcie-switch switch0-downstream-0:switch1
852    --pcie-switch switch1-downstream-1:switch2
853
854    # Enable hotplug on all downstream switch ports of switch0
855    --pcie-switch rp0:switch0,hotplug
856
857Syntax: <port_name>:<name>[,opt,opt=arg,...]
858
859    port_name can be:
860        - Root port name (e.g., "rp0") to connect directly to a root port
861        - Downstream port name (e.g., "switch0-downstream-1") to connect to another switch
862
863Options:
864    `hotplug`                       enable hotplug support for all downstream switch ports
865    `num_downstream_ports=<value>`  number of downstream ports, default 4
866    `acs=<mask>`                    ACS capability bitmask for downstream switch ports
867"#)]
868    #[clap(long, conflicts_with("pcat"))]
869    pub pcie_switch: Vec<GenericPcieSwitchCli>,
870
871    /// Attach a PCIe remote device to a downstream port
872    #[clap(long_help = r#"
873Attach PCIe devices to root ports or downstream switch ports
874which are implemented in a simulator running in a remote process.
875
876Examples:
877    # Attach to root port rc0rp0 with default socket
878    --pcie-remote rc0rp0
879
880    # Attach with custom socket address
881    --pcie-remote rc0rp0,socket=0.0.0.0:48914
882
883    # Specify HU and controller identifiers
884    --pcie-remote rc0rp0,hu=1,controller=0
885
886    # Multiple devices on different ports
887    --pcie-remote rc0rp0,socket=0.0.0.0:48914
888    --pcie-remote rc0rp1,socket=0.0.0.0:48915
889
890Syntax: <port_name>[,opt=arg,...]
891
892Options:
893    `socket=<address>`              TCP socket (default: localhost:48914)
894    `hu=<value>`                    Hardware unit identifier (default: 0)
895    `controller=<value>`            Controller identifier (default: 0)
896"#)]
897    #[clap(long, conflicts_with("pcat"))]
898    pub pcie_remote: Vec<PcieRemoteCli>,
899
900    /// Assign a host PCI device to the guest via VFIO (Linux only)
901    #[clap(long_help = r#"
902Assign a host PCI device to the guest via Linux VFIO.
903
904The device must be bound to vfio-pci on the host before starting the VM.
905
906Examples:
907    # Assign NVMe controller to root port rp0
908    --vfio rp0:0000:01:00.0
909
910Syntax: <port_name>:<pci_bdf>
911
912    port_name    Root port or downstream switch port name
913    pci_bdf      PCI domain:bus:device.function of the VFIO device on
914                 the host (use lspci -D to find it)
915"#)]
916    #[cfg(target_os = "linux")]
917    #[clap(long, conflicts_with("pcat"))]
918    pub vfio: Vec<VfioDeviceCli>,
919}
920
921impl Options {
922    /// Returns the effective guest RAM size.
923    pub fn memory_size(&self) -> u64 {
924        self.memory.mem_size
925    }
926
927    /// Returns whether guest RAM should be prefetched.
928    pub fn prefetch_memory(&self) -> bool {
929        self.memory.prefetch || self.deprecated_prefetch
930    }
931
932    /// Returns whether guest RAM should use private anonymous backing.
933    pub fn private_memory(&self) -> bool {
934        self.memory.shared == Some(false) || self.deprecated_private_memory
935    }
936
937    /// Returns whether guest RAM should be marked THP-eligible.
938    pub fn transparent_hugepages(&self) -> bool {
939        self.memory.transparent_hugepages || self.deprecated_thp
940    }
941
942    /// Returns the effective file backing path for guest RAM.
943    pub fn memory_backing_file(&self) -> Option<&PathBuf> {
944        self.memory
945            .file
946            .as_ref()
947            .or(self.deprecated_memory_backing_file.as_ref())
948    }
949
950    /// Validates combinations that span the new `--memory` parser and legacy aliases.
951    pub fn validate_memory_options(&self) -> anyhow::Result<()> {
952        if self.memory.file.is_some() && self.deprecated_memory_backing_file.is_some() {
953            anyhow::bail!("--memory file=... conflicts with --memory-backing-file");
954        }
955        if self.memory.file.is_some() && self.restore_snapshot.is_some() {
956            anyhow::bail!("--memory file=... conflicts with --restore-snapshot");
957        }
958        if self.memory.shared == Some(true) && self.deprecated_private_memory {
959            anyhow::bail!("--memory shared=on conflicts with --private-memory");
960        }
961        if self.memory_backing_file().is_some() && self.private_memory() {
962            anyhow::bail!("file-backed memory conflicts with private memory");
963        }
964        if self.transparent_hugepages() && !self.private_memory() {
965            anyhow::bail!("transparent huge pages requires private memory mode");
966        }
967        if self.memory.hugepages {
968            if !cfg!(target_os = "linux") {
969                anyhow::bail!("hugepages are only supported on Linux");
970            }
971            if self.private_memory() {
972                anyhow::bail!("hugepages conflict with private memory");
973            }
974            if self.memory_backing_file().is_some() || self.restore_snapshot.is_some() {
975                anyhow::bail!("hugepages conflict with file-backed memory");
976            }
977            if self.pcat {
978                anyhow::bail!("hugepages conflict with x86 legacy RAM splitting");
979            }
980        }
981        Ok(())
982    }
983}
984
985#[derive(Clone, Debug, PartialEq)]
986pub struct FsArgs {
987    pub tag: String,
988    pub path: String,
989    pub pcie_port: Option<String>,
990}
991
992impl FromStr for FsArgs {
993    type Err = anyhow::Error;
994
995    fn from_str(s: &str) -> Result<Self, Self::Err> {
996        let (pcie_port, s) = parse_pcie_port_prefix(s);
997        let mut s = s.split(',');
998        let (Some(tag), Some(path), None) = (s.next(), s.next(), s.next()) else {
999            anyhow::bail!("expected [pcie_port=<port>:]<tag>,<path>");
1000        };
1001        Ok(Self {
1002            tag: tag.to_owned(),
1003            path: path.to_owned(),
1004            pcie_port,
1005        })
1006    }
1007}
1008
1009#[derive(Clone, Debug, PartialEq)]
1010pub struct FsArgsWithOptions {
1011    /// The file system tag.
1012    pub tag: String,
1013    /// The root path.
1014    pub path: String,
1015    /// The extra options, joined with ';'.
1016    pub options: String,
1017    /// Optional PCIe port name.
1018    pub pcie_port: Option<String>,
1019}
1020
1021impl FromStr for FsArgsWithOptions {
1022    type Err = anyhow::Error;
1023
1024    fn from_str(s: &str) -> Result<Self, Self::Err> {
1025        let (pcie_port, s) = parse_pcie_port_prefix(s);
1026        let mut s = s.split(',');
1027        let (Some(tag), Some(path)) = (s.next(), s.next()) else {
1028            anyhow::bail!("expected [pcie_port=<port>:]<tag>,<path>[,<options>]");
1029        };
1030        let options = s.collect::<Vec<_>>().join(";");
1031        Ok(Self {
1032            tag: tag.to_owned(),
1033            path: path.to_owned(),
1034            options,
1035            pcie_port,
1036        })
1037    }
1038}
1039
1040#[derive(Copy, Clone, clap::ValueEnum)]
1041pub enum VirtioBusCli {
1042    Auto,
1043    Mmio,
1044    Pci,
1045    Vpci,
1046}
1047
1048/// Parse an optional `pcie_port=<name>:` prefix from a CLI argument string.
1049///
1050/// Returns `(Some(port_name), rest)` if the prefix is present, or
1051/// `(None, original)` if not.
1052fn parse_pcie_port_prefix(s: &str) -> (Option<String>, &str) {
1053    if let Some(rest) = s.strip_prefix("pcie_port=") {
1054        if let Some((port, rest)) = rest.split_once(':') {
1055            if !port.is_empty() {
1056                return (Some(port.to_string()), rest);
1057            }
1058        }
1059    }
1060    (None, s)
1061}
1062
1063#[derive(Clone, Debug, PartialEq)]
1064pub struct VirtioPmemArgs {
1065    pub path: String,
1066    pub pcie_port: Option<String>,
1067}
1068
1069impl FromStr for VirtioPmemArgs {
1070    type Err = anyhow::Error;
1071
1072    fn from_str(s: &str) -> Result<Self, Self::Err> {
1073        let (pcie_port, s) = parse_pcie_port_prefix(s);
1074        if s.is_empty() {
1075            anyhow::bail!("expected [pcie_port=<port>:]<path>");
1076        }
1077        Ok(Self {
1078            path: s.to_owned(),
1079            pcie_port,
1080        })
1081    }
1082}
1083
1084#[derive(clap::ValueEnum, Clone, Copy)]
1085pub enum SecureBootTemplateCli {
1086    Windows,
1087    UefiCa,
1088}
1089
1090fn parse_memory(s: &str) -> anyhow::Result<u64> {
1091    if s == "VMGS_DEFAULT" {
1092        Ok(vmgs_format::VMGS_DEFAULT_CAPACITY)
1093    } else {
1094        || -> Option<u64> {
1095            let mut b = s.as_bytes();
1096            if s.ends_with('B') {
1097                b = &b[..b.len() - 1]
1098            }
1099            if b.is_empty() {
1100                return None;
1101            }
1102            let multi = match b[b.len() - 1] as char {
1103                'T' => Some(1024 * 1024 * 1024 * 1024),
1104                'G' => Some(1024 * 1024 * 1024),
1105                'M' => Some(1024 * 1024),
1106                'K' => Some(1024),
1107                _ => None,
1108            };
1109            if multi.is_some() {
1110                b = &b[..b.len() - 1]
1111            }
1112            let n: u64 = std::str::from_utf8(b).ok()?.parse().ok()?;
1113            n.checked_mul(multi.unwrap_or(1))
1114        }()
1115        .with_context(|| format!("invalid memory size '{0}'", s))
1116    }
1117}
1118
1119fn parse_acs_capability_mask(value: &str) -> anyhow::Result<u16> {
1120    if let Some(hex) = value
1121        .strip_prefix("0x")
1122        .or_else(|| value.strip_prefix("0X"))
1123    {
1124        u16::from_str_radix(hex, 16).context("invalid ACS capability mask")
1125    } else {
1126        value.parse::<u16>().context("invalid ACS capability mask")
1127    }
1128}
1129
1130fn parse_memory_toggle(key: &str, value: &str) -> anyhow::Result<bool> {
1131    match value {
1132        "on" => Ok(true),
1133        "off" => Ok(false),
1134        _ => anyhow::bail!("invalid {key} value '{value}', expected 'on' or 'off'"),
1135    }
1136}
1137
1138fn parse_memory_config(s: &str) -> anyhow::Result<MemoryCli> {
1139    if !s.contains('=') && !s.contains(',') {
1140        return Ok(MemoryCli {
1141            mem_size: parse_memory(s)?,
1142            shared: None,
1143            prefetch: false,
1144            transparent_hugepages: false,
1145            hugepages: false,
1146            hugepage_size: None,
1147            file: None,
1148        });
1149    }
1150
1151    let mut mem_size = DEFAULT_MEMORY_SIZE;
1152    let mut saw_size = false;
1153    let mut shared = None;
1154    let mut prefetch = None;
1155    let mut transparent_hugepages = None;
1156    let mut hugepages = None;
1157    let mut hugepage_size = None;
1158    let mut file = None;
1159
1160    for part in s.split(',') {
1161        let (key, value) = part
1162            .split_once('=')
1163            .with_context(|| format!("invalid memory option '{part}', expected key=value"))?;
1164        if key.is_empty() || value.is_empty() {
1165            anyhow::bail!("invalid memory option '{part}', expected key=value");
1166        }
1167
1168        match key {
1169            "size" => {
1170                if saw_size {
1171                    anyhow::bail!("duplicate memory option 'size'");
1172                }
1173                mem_size = parse_memory(value)?;
1174                saw_size = true;
1175            }
1176            "shared" => {
1177                if shared.is_some() {
1178                    anyhow::bail!("duplicate memory option 'shared'");
1179                }
1180                shared = Some(parse_memory_toggle(key, value)?);
1181            }
1182            "prefetch" => {
1183                if prefetch.is_some() {
1184                    anyhow::bail!("duplicate memory option 'prefetch'");
1185                }
1186                prefetch = Some(parse_memory_toggle(key, value)?);
1187            }
1188            "thp" => {
1189                if transparent_hugepages.is_some() {
1190                    anyhow::bail!("duplicate memory option 'thp'");
1191                }
1192                transparent_hugepages = Some(parse_memory_toggle(key, value)?);
1193            }
1194            "hugepages" => {
1195                if hugepages.is_some() {
1196                    anyhow::bail!("duplicate memory option 'hugepages'");
1197                }
1198                hugepages = Some(parse_memory_toggle(key, value)?);
1199            }
1200            "hugepage_size" => {
1201                if hugepage_size.is_some() {
1202                    anyhow::bail!("duplicate memory option 'hugepage_size'");
1203                }
1204                hugepage_size = Some(parse_memory(value)?);
1205            }
1206            "file" => {
1207                if file.is_some() {
1208                    anyhow::bail!("duplicate memory option 'file'");
1209                }
1210                file = Some(PathBuf::from(value));
1211            }
1212            _ => anyhow::bail!("unknown memory option '{key}'"),
1213        }
1214    }
1215
1216    if transparent_hugepages == Some(true) && shared != Some(false) {
1217        anyhow::bail!("memory thp=on requires shared=off");
1218    }
1219    if hugepage_size.is_some() && hugepages != Some(true) {
1220        anyhow::bail!("memory hugepage_size requires hugepages=on");
1221    }
1222    if hugepages == Some(true) {
1223        if shared == Some(false) {
1224            anyhow::bail!("memory hugepages=on conflicts with shared=off");
1225        }
1226        if file.is_some() {
1227            anyhow::bail!("memory hugepages=on conflicts with file=...");
1228        }
1229    }
1230
1231    Ok(MemoryCli {
1232        mem_size,
1233        shared,
1234        prefetch: prefetch.unwrap_or(false),
1235        transparent_hugepages: transparent_hugepages.unwrap_or(false),
1236        hugepages: hugepages.unwrap_or(false),
1237        hugepage_size,
1238        file,
1239    })
1240}
1241
1242/// Parse a number from a string that could be prefixed with 0x to indicate hex.
1243fn parse_number(s: &str) -> Result<u64, std::num::ParseIntError> {
1244    match s.strip_prefix("0x") {
1245        Some(rest) => u64::from_str_radix(rest, 16),
1246        None => s.parse::<u64>(),
1247    }
1248}
1249
1250#[derive(Clone, Debug, PartialEq)]
1251pub enum DiskCliKind {
1252    // mem:<len>
1253    Memory(u64),
1254    // memdiff:<kind>
1255    MemoryDiff(Box<DiskCliKind>),
1256    // sql:<path>[;create=<len>]
1257    Sqlite {
1258        path: PathBuf,
1259        create_with_len: Option<u64>,
1260    },
1261    // sqldiff:<path>[;create]:<kind>
1262    SqliteDiff {
1263        path: PathBuf,
1264        create: bool,
1265        disk: Box<DiskCliKind>,
1266    },
1267    // autocache:[key]:<kind>
1268    AutoCacheSqlite {
1269        cache_path: String,
1270        key: Option<String>,
1271        disk: Box<DiskCliKind>,
1272    },
1273    // prwrap:<kind>
1274    PersistentReservationsWrapper(Box<DiskCliKind>),
1275    // file:<path>[;direct][;create=<len>]
1276    File {
1277        path: PathBuf,
1278        create_with_len: Option<u64>,
1279        direct: bool,
1280    },
1281    // blob:<type>:<url>
1282    Blob {
1283        kind: BlobKind,
1284        url: String,
1285    },
1286    // crypt:<cipher>:<key_file>:<kind>
1287    Crypt {
1288        cipher: DiskCipher,
1289        key_file: PathBuf,
1290        disk: Box<DiskCliKind>,
1291    },
1292    // delay:<delay_ms>:<kind>
1293    DelayDiskWrapper {
1294        delay_ms: u64,
1295        disk: Box<DiskCliKind>,
1296    },
1297}
1298
1299#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]
1300pub enum DiskCipher {
1301    #[clap(name = "xts-aes-256")]
1302    XtsAes256,
1303}
1304
1305#[derive(Copy, Clone, Debug, PartialEq)]
1306pub enum BlobKind {
1307    Flat,
1308    Vhd1,
1309}
1310
1311struct FileOpts {
1312    path: PathBuf,
1313    create_with_len: Option<u64>,
1314    direct: bool,
1315}
1316
1317fn parse_file_opts(arg: &str) -> anyhow::Result<FileOpts> {
1318    let mut path = arg;
1319    let mut create_with_len = None;
1320    let mut direct = false;
1321
1322    // Parse semicolon-delimited options after the path.
1323    if let Some((p, rest)) = arg.split_once(';') {
1324        path = p;
1325        for opt in rest.split(';') {
1326            if let Some(len) = opt.strip_prefix("create=") {
1327                create_with_len = Some(parse_memory(len)?);
1328            } else if opt == "direct" {
1329                direct = true;
1330            } else {
1331                anyhow::bail!("invalid file option '{opt}', expected 'create=<len>' or 'direct'");
1332            }
1333        }
1334    }
1335
1336    Ok(FileOpts {
1337        path: path.into(),
1338        create_with_len,
1339        direct,
1340    })
1341}
1342
1343impl DiskCliKind {
1344    /// Parse an `autocache:[key]:<kind>` disk spec, given the cache path
1345    /// (normally read from `OPENVMM_AUTO_CACHE_PATH`).
1346    fn parse_autocache(
1347        arg: &str,
1348        cache_path: Result<String, std::env::VarError>,
1349    ) -> anyhow::Result<Self> {
1350        let (key, kind) = arg.split_once(':').context("expected [key]:kind")?;
1351        let cache_path = cache_path.context("must set cache path via OPENVMM_AUTO_CACHE_PATH")?;
1352        Ok(DiskCliKind::AutoCacheSqlite {
1353            cache_path,
1354            key: (!key.is_empty()).then(|| key.to_string()),
1355            disk: Box::new(kind.parse()?),
1356        })
1357    }
1358}
1359
1360impl FromStr for DiskCliKind {
1361    type Err = anyhow::Error;
1362
1363    fn from_str(s: &str) -> anyhow::Result<Self> {
1364        let disk = match s.split_once(':') {
1365            // convenience support for passing bare paths as file disks
1366            None => {
1367                let FileOpts {
1368                    path,
1369                    create_with_len,
1370                    direct,
1371                } = parse_file_opts(s)?;
1372                DiskCliKind::File {
1373                    path,
1374                    create_with_len,
1375                    direct,
1376                }
1377            }
1378            Some((kind, arg)) => match kind {
1379                "mem" => DiskCliKind::Memory(parse_memory(arg)?),
1380                "memdiff" => DiskCliKind::MemoryDiff(Box::new(arg.parse()?)),
1381                "sql" => {
1382                    let FileOpts {
1383                        path,
1384                        create_with_len,
1385                        direct,
1386                    } = parse_file_opts(arg)?;
1387                    if direct {
1388                        anyhow::bail!("'direct' is not supported for 'sql' disks");
1389                    }
1390                    DiskCliKind::Sqlite {
1391                        path,
1392                        create_with_len,
1393                    }
1394                }
1395                "sqldiff" => {
1396                    let (path_and_opts, kind) =
1397                        arg.split_once(':').context("expected path[;opts]:kind")?;
1398                    let disk = Box::new(kind.parse()?);
1399                    match path_and_opts.split_once(';') {
1400                        Some((path, create)) => {
1401                            if create != "create" {
1402                                anyhow::bail!("invalid syntax after ';', expected 'create'")
1403                            }
1404                            DiskCliKind::SqliteDiff {
1405                                path: path.into(),
1406                                create: true,
1407                                disk,
1408                            }
1409                        }
1410                        None => DiskCliKind::SqliteDiff {
1411                            path: path_and_opts.into(),
1412                            create: false,
1413                            disk,
1414                        },
1415                    }
1416                }
1417                "autocache" => {
1418                    Self::parse_autocache(arg, std::env::var("OPENVMM_AUTO_CACHE_PATH"))?
1419                }
1420                "prwrap" => DiskCliKind::PersistentReservationsWrapper(Box::new(arg.parse()?)),
1421                "file" => {
1422                    let FileOpts {
1423                        path,
1424                        create_with_len,
1425                        direct,
1426                    } = parse_file_opts(arg)?;
1427                    DiskCliKind::File {
1428                        path,
1429                        create_with_len,
1430                        direct,
1431                    }
1432                }
1433                "blob" => {
1434                    let (blob_kind, url) = arg.split_once(':').context("expected kind:url")?;
1435                    let blob_kind = match blob_kind {
1436                        "flat" => BlobKind::Flat,
1437                        "vhd1" => BlobKind::Vhd1,
1438                        _ => anyhow::bail!("unknown blob kind {blob_kind}"),
1439                    };
1440                    DiskCliKind::Blob {
1441                        kind: blob_kind,
1442                        url: url.to_string(),
1443                    }
1444                }
1445                "crypt" => {
1446                    let (cipher, (key, kind)) = arg
1447                        .split_once(':')
1448                        .and_then(|(cipher, arg)| Some((cipher, arg.split_once(':')?)))
1449                        .context("expected cipher:key_file:kind")?;
1450                    DiskCliKind::Crypt {
1451                        cipher: ValueEnum::from_str(cipher, false)
1452                            .map_err(|err| anyhow::anyhow!("invalid cipher: {err}"))?,
1453                        key_file: PathBuf::from(key),
1454                        disk: Box::new(kind.parse()?),
1455                    }
1456                }
1457                kind => {
1458                    // here's a fun edge case: what if the user passes `--disk d:\path\to\disk.img`?
1459                    //
1460                    // in this case, we actually want to treat that leading `d:` as part of the
1461                    // path, rather than as a disk with `kind == 'd'`
1462                    let FileOpts {
1463                        path,
1464                        create_with_len,
1465                        direct,
1466                    } = parse_file_opts(s)?;
1467                    if path.has_root() {
1468                        DiskCliKind::File {
1469                            path,
1470                            create_with_len,
1471                            direct,
1472                        }
1473                    } else {
1474                        anyhow::bail!("invalid disk kind {kind}");
1475                    }
1476                }
1477            },
1478        };
1479        Ok(disk)
1480    }
1481}
1482
1483#[derive(Clone)]
1484pub struct VmgsCli {
1485    pub kind: DiskCliKind,
1486    pub provision: ProvisionVmgs,
1487}
1488
1489#[derive(Copy, Clone)]
1490pub enum ProvisionVmgs {
1491    OnEmpty,
1492    OnFailure,
1493    True,
1494}
1495
1496impl FromStr for VmgsCli {
1497    type Err = anyhow::Error;
1498
1499    fn from_str(s: &str) -> anyhow::Result<Self> {
1500        let (kind, opt) = s
1501            .split_once(',')
1502            .map(|(k, o)| (k, Some(o)))
1503            .unwrap_or((s, None));
1504        let kind = kind.parse()?;
1505
1506        let provision = match opt {
1507            None => ProvisionVmgs::OnEmpty,
1508            Some("fmt-on-fail") => ProvisionVmgs::OnFailure,
1509            Some("fmt") => ProvisionVmgs::True,
1510            Some(opt) => anyhow::bail!("unknown option: '{opt}'"),
1511        };
1512
1513        Ok(VmgsCli { kind, provision })
1514    }
1515}
1516
1517// <kind>[,ro]
1518#[derive(Clone)]
1519pub struct DiskCli {
1520    pub vtl: DeviceVtl,
1521    pub kind: DiskCliKind,
1522    pub read_only: bool,
1523    pub is_dvd: bool,
1524    pub underhill: Option<UnderhillDiskSource>,
1525    pub pcie_port: Option<String>,
1526}
1527
1528#[derive(Copy, Clone)]
1529pub enum UnderhillDiskSource {
1530    Scsi,
1531    Nvme,
1532}
1533
1534impl FromStr for DiskCli {
1535    type Err = anyhow::Error;
1536
1537    fn from_str(s: &str) -> anyhow::Result<Self> {
1538        let mut opts = s.split(',');
1539        let kind = opts.next().unwrap().parse()?;
1540
1541        let mut read_only = false;
1542        let mut is_dvd = false;
1543        let mut underhill = None;
1544        let mut vtl = DeviceVtl::Vtl0;
1545        let mut pcie_port = None;
1546        for opt in opts {
1547            let mut s = opt.split('=');
1548            let opt = s.next().unwrap();
1549            match opt {
1550                "ro" => read_only = true,
1551                "dvd" => {
1552                    is_dvd = true;
1553                    read_only = true;
1554                }
1555                "vtl2" => {
1556                    vtl = DeviceVtl::Vtl2;
1557                }
1558                "uh" => underhill = Some(UnderhillDiskSource::Scsi),
1559                "uh-nvme" => underhill = Some(UnderhillDiskSource::Nvme),
1560                "pcie_port" => {
1561                    let port = s.next();
1562                    if port.is_none_or(|p| p.is_empty()) {
1563                        anyhow::bail!("`pcie_port` requires a port name");
1564                    }
1565                    pcie_port = Some(String::from(port.unwrap()));
1566                }
1567                opt => anyhow::bail!("unknown option: '{opt}'"),
1568            }
1569        }
1570
1571        if underhill.is_some() && vtl != DeviceVtl::Vtl0 {
1572            anyhow::bail!("`uh` or `uh-nvme` is incompatible with `vtl2`");
1573        }
1574
1575        if pcie_port.is_some() && (underhill.is_some() || vtl != DeviceVtl::Vtl0 || is_dvd) {
1576            anyhow::bail!("`pcie_port` is incompatible with `uh`, `uh-nvme`, `vtl2`, and `dvd`");
1577        }
1578
1579        Ok(DiskCli {
1580            vtl,
1581            kind,
1582            read_only,
1583            is_dvd,
1584            underhill,
1585            pcie_port,
1586        })
1587    }
1588}
1589
1590// <kind>[,ro,s]
1591#[derive(Clone)]
1592pub struct IdeDiskCli {
1593    pub kind: DiskCliKind,
1594    pub read_only: bool,
1595    pub channel: Option<u8>,
1596    pub device: Option<u8>,
1597    pub is_dvd: bool,
1598}
1599
1600impl FromStr for IdeDiskCli {
1601    type Err = anyhow::Error;
1602
1603    fn from_str(s: &str) -> anyhow::Result<Self> {
1604        let mut opts = s.split(',');
1605        let kind = opts.next().unwrap().parse()?;
1606
1607        let mut read_only = false;
1608        let mut channel = None;
1609        let mut device = None;
1610        let mut is_dvd = false;
1611        for opt in opts {
1612            let mut s = opt.split('=');
1613            let opt = s.next().unwrap();
1614            match opt {
1615                "ro" => read_only = true,
1616                "p" => channel = Some(0),
1617                "s" => channel = Some(1),
1618                "0" => device = Some(0),
1619                "1" => device = Some(1),
1620                "dvd" => {
1621                    is_dvd = true;
1622                    read_only = true;
1623                }
1624                _ => anyhow::bail!("unknown option: '{opt}'"),
1625            }
1626        }
1627
1628        Ok(IdeDiskCli {
1629            kind,
1630            read_only,
1631            channel,
1632            device,
1633            is_dvd,
1634        })
1635    }
1636}
1637
1638// <kind>[,ro]
1639#[derive(Clone, Debug, PartialEq)]
1640pub struct FloppyDiskCli {
1641    pub kind: DiskCliKind,
1642    pub read_only: bool,
1643}
1644
1645impl FromStr for FloppyDiskCli {
1646    type Err = anyhow::Error;
1647
1648    fn from_str(s: &str) -> anyhow::Result<Self> {
1649        if s.is_empty() {
1650            anyhow::bail!("empty disk spec");
1651        }
1652        let mut opts = s.split(',');
1653        let kind = opts.next().unwrap().parse()?;
1654
1655        let mut read_only = false;
1656        for opt in opts {
1657            let mut s = opt.split('=');
1658            let opt = s.next().unwrap();
1659            match opt {
1660                "ro" => read_only = true,
1661                _ => anyhow::bail!("unknown option: '{opt}'"),
1662            }
1663        }
1664
1665        Ok(FloppyDiskCli { kind, read_only })
1666    }
1667}
1668
1669#[derive(Clone)]
1670pub struct DebugconSerialConfigCli {
1671    pub port: u16,
1672    pub serial: SerialConfigCli,
1673}
1674
1675impl FromStr for DebugconSerialConfigCli {
1676    type Err = String;
1677
1678    fn from_str(s: &str) -> Result<Self, Self::Err> {
1679        let Some((port, serial)) = s.split_once(',') else {
1680            return Err("invalid format (missing comma between port and serial)".into());
1681        };
1682
1683        let port: u16 = parse_number(port)
1684            .map_err(|_| "could not parse port".to_owned())?
1685            .try_into()
1686            .map_err(|_| "port must be 16-bit")?;
1687        let serial: SerialConfigCli = serial.parse()?;
1688
1689        Ok(Self { port, serial })
1690    }
1691}
1692
1693/// (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>]\[,name=\<windowtitle\>\] | none)
1694#[derive(Clone, Debug, PartialEq)]
1695pub enum SerialConfigCli {
1696    None,
1697    Console,
1698    NewConsole(Option<PathBuf>, Option<String>),
1699    Stderr,
1700    Pipe(PathBuf),
1701    Tcp(SocketAddr),
1702    File(PathBuf),
1703}
1704
1705impl FromStr for SerialConfigCli {
1706    type Err = String;
1707
1708    fn from_str(s: &str) -> Result<Self, Self::Err> {
1709        let keyvalues = SerialConfigCli::parse_keyvalues(s)?;
1710
1711        let first_key = match keyvalues.first() {
1712            Some(first_pair) => first_pair.0.as_str(),
1713            None => Err("invalid serial configuration: no values supplied")?,
1714        };
1715        let first_value = keyvalues.first().unwrap().1.as_ref();
1716
1717        let ret = match first_key {
1718            "none" => SerialConfigCli::None,
1719            "console" => SerialConfigCli::Console,
1720            "stderr" => SerialConfigCli::Stderr,
1721            "file" => match first_value {
1722                Some(path) => SerialConfigCli::File(path.into()),
1723                None => Err("invalid serial configuration: file requires a value")?,
1724            },
1725            "term" => {
1726                // If user supplies a name key, use it to title the window
1727                let window_name = keyvalues.iter().find(|(key, _)| key == "name");
1728                let window_name = match window_name {
1729                    Some((_, Some(name))) => Some(name.clone()),
1730                    _ => None,
1731                };
1732
1733                SerialConfigCli::NewConsole(first_value.map(|p| p.into()), window_name)
1734            }
1735            "listen" => match first_value {
1736                Some(path) => {
1737                    if let Some(tcp) = path.strip_prefix("tcp:") {
1738                        let addr = tcp
1739                            .parse()
1740                            .map_err(|err| format!("invalid tcp address: {err}"))?;
1741                        SerialConfigCli::Tcp(addr)
1742                    } else {
1743                        SerialConfigCli::Pipe(path.into())
1744                    }
1745                }
1746                None => Err(
1747                    "invalid serial configuration: listen requires a value of tcp:addr or pipe",
1748                )?,
1749            },
1750            _ => {
1751                return Err(format!(
1752                    "invalid serial configuration: '{}' is not a known option",
1753                    first_key
1754                ));
1755            }
1756        };
1757
1758        Ok(ret)
1759    }
1760}
1761
1762impl SerialConfigCli {
1763    /// Parse a comma separated list of key=value options into a vector of
1764    /// key/value pairs.
1765    fn parse_keyvalues(s: &str) -> Result<Vec<(String, Option<String>)>, String> {
1766        let mut ret = Vec::new();
1767
1768        // For each comma separated item in the supplied list
1769        for item in s.split(',') {
1770            // Split on the = for key and value
1771            // If no = is found, treat key as key and value as None
1772            let mut eqsplit = item.split('=');
1773            let key = eqsplit.next();
1774            let value = eqsplit.next();
1775
1776            if let Some(key) = key {
1777                ret.push((key.to_owned(), value.map(|x| x.to_owned())));
1778            } else {
1779                // An empty key is invalid
1780                return Err("invalid key=value pair in serial config".into());
1781            }
1782        }
1783        Ok(ret)
1784    }
1785}
1786
1787#[derive(Clone, Debug, PartialEq)]
1788pub enum EndpointConfigCli {
1789    None,
1790    Consomme {
1791        cidr: Option<String>,
1792        host_fwd: Vec<HostPortConfigCli>,
1793    },
1794    Dio {
1795        id: Option<String>,
1796    },
1797    Tap {
1798        name: String,
1799    },
1800}
1801
1802/// Parsed host port forwarding configuration from the CLI.
1803#[derive(Clone, Debug, PartialEq)]
1804pub struct HostPortConfigCli {
1805    pub protocol: HostPortProtocolCli,
1806    pub host_address: Option<std::net::IpAddr>,
1807    pub host_port: u16,
1808    pub guest_port: u16,
1809}
1810
1811/// Protocol for host port forwarding.
1812#[derive(Clone, Debug, PartialEq)]
1813pub enum HostPortProtocolCli {
1814    Tcp,
1815    Udp,
1816}
1817
1818fn parse_hostfwd(s: &str) -> Result<HostPortConfigCli, String> {
1819    // Format: protocol:[hostaddr]:hostport-[guestaddr]:guestport
1820    // Examples: "tcp::3389-:3389", "tcp:127.0.0.1:8080-:80", "tcp:[::1]:8080-:80"
1821    let (host_part, guest_part) = s.split_once('-').ok_or_else(|| {
1822        format!(
1823            "invalid hostfwd format '{s}', \
1824             expected 'proto:[hostaddr]:hostport-[guestaddr]:guestport'"
1825        )
1826    })?;
1827
1828    // Extract protocol from host part (first colon-delimited field)
1829    let (proto, host_addr_port) = host_part.split_once(':').ok_or_else(|| {
1830        format!("invalid hostfwd host part '{host_part}', expected 'proto:[hostaddr]:hostport'")
1831    })?;
1832    let protocol = match proto {
1833        "tcp" => HostPortProtocolCli::Tcp,
1834        "udp" => HostPortProtocolCli::Udp,
1835        other => {
1836            return Err(format!(
1837                "unknown hostfwd protocol '{other}', expected 'tcp' or 'udp'"
1838            ));
1839        }
1840    };
1841
1842    let (host_address, host_port) = parse_addr_port(host_addr_port)
1843        .map_err(|e| format!("invalid hostfwd host address/port: {e}"))?;
1844    let (_, guest_port) = parse_addr_port(guest_part)
1845        .map_err(|e| format!("invalid hostfwd guest address/port: {e}"))?;
1846
1847    Ok(HostPortConfigCli {
1848        protocol,
1849        host_address,
1850        host_port,
1851        guest_port,
1852    })
1853}
1854
1855/// Parse an address-port pair in one of these forms:
1856/// - `[ipv6addr]:port`
1857/// - `addr:port`
1858/// - `:port`  (empty address)
1859/// - `port`   (no address)
1860fn parse_addr_port(s: &str) -> Result<(Option<std::net::IpAddr>, u16), String> {
1861    if let Some(rest) = s.strip_prefix('[') {
1862        // Bracketed IPv6 address: [addr]:port
1863        let (addr, port) = rest
1864            .split_once("]:")
1865            .ok_or_else(|| format!("expected '[addr]:port', got '[{rest}'"))?;
1866        let port: u16 = port.parse().map_err(|_| format!("invalid port '{port}'"))?;
1867        let addr: std::net::IpAddr = addr
1868            .parse()
1869            .map_err(|e| format!("invalid address '{addr}': {e}"))?;
1870        Ok((Some(addr), port))
1871    } else {
1872        match s.rsplit_once(':') {
1873            Some((addr, port)) => {
1874                let port: u16 = port.parse().map_err(|_| format!("invalid port '{port}'"))?;
1875                let addr = if addr.is_empty() {
1876                    None
1877                } else {
1878                    let parsed: std::net::IpAddr = addr
1879                        .parse()
1880                        .map_err(|e| format!("invalid address '{addr}': {e}"))?;
1881                    Some(parsed)
1882                };
1883                Ok((addr, port))
1884            }
1885            None => {
1886                let port: u16 = s.parse().map_err(|_| format!("invalid port '{s}'"))?;
1887                Ok((None, port))
1888            }
1889        }
1890    }
1891}
1892
1893impl FromStr for EndpointConfigCli {
1894    type Err = String;
1895
1896    fn from_str(s: &str) -> Result<Self, Self::Err> {
1897        let ret = match s.split(':').collect::<Vec<_>>().as_slice() {
1898            ["none"] => EndpointConfigCli::None,
1899            ["consomme", rest @ ..] => {
1900                let remaining = rest.join(":");
1901                let mut cidr = None;
1902                let mut host_fwd = Vec::new();
1903                for opt in remaining.split(',').filter(|s| !s.is_empty()) {
1904                    if let Some(fwd) = opt.strip_prefix("hostfwd=") {
1905                        host_fwd.push(parse_hostfwd(fwd)?);
1906                    } else if cidr.is_none() {
1907                        cidr = Some(opt.to_owned());
1908                    } else {
1909                        return Err(format!("unexpected consomme option '{opt}'"));
1910                    }
1911                }
1912                EndpointConfigCli::Consomme { cidr, host_fwd }
1913            }
1914            ["dio", s @ ..] => EndpointConfigCli::Dio {
1915                id: s.first().map(|s| (*s).to_owned()),
1916            },
1917            ["tap", name] => EndpointConfigCli::Tap {
1918                name: (*name).to_owned(),
1919            },
1920            _ => return Err("invalid network backend".into()),
1921        };
1922
1923        Ok(ret)
1924    }
1925}
1926
1927#[derive(Clone, Debug, PartialEq)]
1928pub struct NicConfigCli {
1929    pub vtl: DeviceVtl,
1930    pub endpoint: EndpointConfigCli,
1931    pub max_queues: Option<u16>,
1932    pub underhill: bool,
1933    pub pcie_port: Option<String>,
1934}
1935
1936impl FromStr for NicConfigCli {
1937    type Err = String;
1938
1939    fn from_str(mut s: &str) -> Result<Self, Self::Err> {
1940        let mut vtl = DeviceVtl::Vtl0;
1941        let mut max_queues = None;
1942        let mut underhill = false;
1943        let mut pcie_port = None;
1944        while let Some((opt, rest)) = s.split_once(':') {
1945            if let Some((opt, val)) = opt.split_once('=') {
1946                match opt {
1947                    "queues" => {
1948                        max_queues = Some(val.parse().map_err(|_| "failed to parse queue count")?);
1949                    }
1950                    "pcie_port" => {
1951                        if val.is_empty() {
1952                            return Err("`pcie_port=` requires port name argument".into());
1953                        }
1954                        pcie_port = Some(val.to_string());
1955                    }
1956                    _ => break,
1957                }
1958            } else {
1959                match opt {
1960                    "vtl2" => {
1961                        vtl = DeviceVtl::Vtl2;
1962                    }
1963                    "uh" => underhill = true,
1964                    _ => break,
1965                }
1966            }
1967            s = rest;
1968        }
1969
1970        if underhill && vtl != DeviceVtl::Vtl0 {
1971            return Err("`uh` is incompatible with `vtl2`".into());
1972        }
1973
1974        if pcie_port.is_some() && (underhill || vtl != DeviceVtl::Vtl0) {
1975            return Err("`pcie_port` is incompatible with `uh` and `vtl2`".into());
1976        }
1977
1978        let endpoint = s.parse()?;
1979        Ok(NicConfigCli {
1980            vtl,
1981            endpoint,
1982            max_queues,
1983            underhill,
1984            pcie_port,
1985        })
1986    }
1987}
1988
1989#[derive(Debug, Error)]
1990#[error("unknown VTL2 relocation type: {0}")]
1991pub struct UnknownVtl2RelocationType(String);
1992
1993fn parse_vtl2_relocation(s: &str) -> Result<Vtl2BaseAddressType, UnknownVtl2RelocationType> {
1994    match s {
1995        "disable" => Ok(Vtl2BaseAddressType::File),
1996        s if s.starts_with("auto=") => {
1997            let s = s.strip_prefix("auto=").unwrap_or_default();
1998            let size = if s == "filesize" {
1999                None
2000            } else {
2001                let size = parse_memory(s).map_err(|e| {
2002                    UnknownVtl2RelocationType(format!(
2003                        "unable to parse memory size from {} for 'auto=' type, {e}",
2004                        e
2005                    ))
2006                })?;
2007                Some(size)
2008            };
2009            Ok(Vtl2BaseAddressType::MemoryLayout { size })
2010        }
2011        s if s.starts_with("absolute=") => {
2012            let s = s.strip_prefix("absolute=");
2013            let addr = parse_number(s.unwrap_or_default()).map_err(|e| {
2014                UnknownVtl2RelocationType(format!(
2015                    "unable to parse number from {} for 'absolute=' type",
2016                    e
2017                ))
2018            })?;
2019            Ok(Vtl2BaseAddressType::Absolute(addr))
2020        }
2021        s if s.starts_with("vtl2=") => {
2022            let s = s.strip_prefix("vtl2=").unwrap_or_default();
2023            let size = if s == "filesize" {
2024                None
2025            } else {
2026                let size = parse_memory(s).map_err(|e| {
2027                    UnknownVtl2RelocationType(format!(
2028                        "unable to parse memory size from {} for 'vtl2=' type, {e}",
2029                        e
2030                    ))
2031                })?;
2032                Some(size)
2033            };
2034            Ok(Vtl2BaseAddressType::Vtl2Allocate { size })
2035        }
2036        _ => Err(UnknownVtl2RelocationType(s.to_owned())),
2037    }
2038}
2039
2040#[derive(Debug, Copy, Clone, PartialEq)]
2041pub enum SmtConfigCli {
2042    Auto,
2043    Force,
2044    Off,
2045}
2046
2047#[derive(Debug, Error)]
2048#[error("expected auto, force, or off")]
2049pub struct BadSmtConfig;
2050
2051impl FromStr for SmtConfigCli {
2052    type Err = BadSmtConfig;
2053
2054    fn from_str(s: &str) -> Result<Self, Self::Err> {
2055        let r = match s {
2056            "auto" => Self::Auto,
2057            "force" => Self::Force,
2058            "off" => Self::Off,
2059            _ => return Err(BadSmtConfig),
2060        };
2061        Ok(r)
2062    }
2063}
2064
2065#[cfg_attr(not(guest_arch = "x86_64"), expect(dead_code))]
2066fn parse_x2apic(s: &str) -> Result<X2ApicConfig, &'static str> {
2067    let r = match s {
2068        "auto" => X2ApicConfig::Auto,
2069        "supported" => X2ApicConfig::Supported,
2070        "off" => X2ApicConfig::Unsupported,
2071        "on" => X2ApicConfig::Enabled,
2072        _ => return Err("expected auto, supported, off, or on"),
2073    };
2074    Ok(r)
2075}
2076
2077#[derive(Debug, Copy, Clone, ValueEnum)]
2078pub enum Vtl0LateMapPolicyCli {
2079    Off,
2080    Log,
2081    Halt,
2082    Exception,
2083}
2084
2085/// PCIe MSI controller selection for aarch64.
2086#[derive(Debug, Copy, Clone, Default, ValueEnum)]
2087pub enum GicMsiCli {
2088    /// Use ITS when available, fall back to GICv2m.
2089    #[default]
2090    Auto,
2091    /// Force GICv3 ITS (LPI-based MSIs).
2092    Its,
2093    /// Force GICv2m (SPI-based MSIs).
2094    V2m,
2095}
2096
2097#[derive(Debug, Copy, Clone, ValueEnum)]
2098pub enum IsolationCli {
2099    Vbs,
2100}
2101
2102#[derive(Debug, Copy, Clone, PartialEq)]
2103pub struct PcatBootOrderCli(pub [PcatBootDevice; 4]);
2104
2105impl FromStr for PcatBootOrderCli {
2106    type Err = &'static str;
2107
2108    fn from_str(s: &str) -> Result<Self, Self::Err> {
2109        let mut default_order = DEFAULT_PCAT_BOOT_ORDER.map(Some);
2110        let mut order = Vec::new();
2111
2112        for item in s.split(',') {
2113            let device = match item {
2114                "optical" => PcatBootDevice::Optical,
2115                "hdd" => PcatBootDevice::HardDrive,
2116                "net" => PcatBootDevice::Network,
2117                "floppy" => PcatBootDevice::Floppy,
2118                _ => return Err("unknown boot device type"),
2119            };
2120
2121            let default_pos = default_order
2122                .iter()
2123                .position(|x| x == &Some(device))
2124                .ok_or("cannot pass duplicate boot devices")?;
2125
2126            order.push(default_order[default_pos].take().unwrap());
2127        }
2128
2129        order.extend(default_order.into_iter().flatten());
2130        assert_eq!(order.len(), 4);
2131
2132        Ok(Self(order.try_into().unwrap()))
2133    }
2134}
2135
2136#[derive(Copy, Clone, Debug, ValueEnum)]
2137pub enum UefiConsoleModeCli {
2138    Default,
2139    Com1,
2140    Com2,
2141    None,
2142}
2143
2144#[derive(Copy, Clone, Debug, Default, ValueEnum)]
2145pub enum EfiDiagnosticsLogLevelCli {
2146    #[default]
2147    Default,
2148    Info,
2149    Full,
2150}
2151
2152#[derive(Clone, Debug, PartialEq)]
2153pub struct PcieRootComplexCli {
2154    pub name: String,
2155    pub segment: u16,
2156    pub start_bus: u8,
2157    pub end_bus: u8,
2158    pub low_mmio: u32,
2159    pub high_mmio: u64,
2160}
2161
2162impl FromStr for PcieRootComplexCli {
2163    type Err = anyhow::Error;
2164
2165    fn from_str(s: &str) -> Result<Self, Self::Err> {
2166        const DEFAULT_PCIE_CRS_LOW_SIZE: u32 = 64 * 1024 * 1024; // 64M
2167        const DEFAULT_PCIE_CRS_HIGH_SIZE: u64 = 1024 * 1024 * 1024; // 1G
2168
2169        let mut opts = s.split(',');
2170        let name = opts.next().context("expected root complex name")?;
2171        if name.is_empty() {
2172            anyhow::bail!("must provide a root complex name");
2173        }
2174
2175        let mut segment = 0;
2176        let mut start_bus = 0;
2177        let mut end_bus = 255;
2178        let mut low_mmio = DEFAULT_PCIE_CRS_LOW_SIZE;
2179        let mut high_mmio = DEFAULT_PCIE_CRS_HIGH_SIZE;
2180        for opt in opts {
2181            let mut s = opt.split('=');
2182            let opt = s.next().context("expected option")?;
2183            match opt {
2184                "segment" => {
2185                    let seg_str = s.next().context("expected segment number")?;
2186                    segment = u16::from_str(seg_str).context("failed to parse segment number")?;
2187                }
2188                "start_bus" => {
2189                    let bus_str = s.next().context("expected start bus number")?;
2190                    start_bus =
2191                        u8::from_str(bus_str).context("failed to parse start bus number")?;
2192                }
2193                "end_bus" => {
2194                    let bus_str = s.next().context("expected end bus number")?;
2195                    end_bus = u8::from_str(bus_str).context("failed to parse end bus number")?;
2196                }
2197                "low_mmio" => {
2198                    let low_mmio_str = s.next().context("expected low MMIO size")?;
2199                    low_mmio = parse_memory(low_mmio_str)
2200                        .context("failed to parse low MMIO size")?
2201                        .try_into()?;
2202                }
2203                "high_mmio" => {
2204                    let high_mmio_str = s.next().context("expected high MMIO size")?;
2205                    high_mmio =
2206                        parse_memory(high_mmio_str).context("failed to parse high MMIO size")?;
2207                }
2208                opt => anyhow::bail!("unknown option: '{opt}'"),
2209            }
2210        }
2211
2212        if start_bus >= end_bus {
2213            anyhow::bail!("start_bus must be less than or equal to end_bus");
2214        }
2215
2216        Ok(PcieRootComplexCli {
2217            name: name.to_string(),
2218            segment,
2219            start_bus,
2220            end_bus,
2221            low_mmio,
2222            high_mmio,
2223        })
2224    }
2225}
2226
2227#[derive(Clone, Debug, PartialEq)]
2228pub struct PcieRootPortCli {
2229    pub root_complex_name: String,
2230    pub name: String,
2231    pub hotplug: bool,
2232    pub acs_capabilities_supported: Option<u16>,
2233}
2234
2235impl FromStr for PcieRootPortCli {
2236    type Err = anyhow::Error;
2237
2238    fn from_str(s: &str) -> Result<Self, Self::Err> {
2239        let mut opts = s.split(',');
2240        let names = opts.next().context("expected root port identifiers")?;
2241        if names.is_empty() {
2242            anyhow::bail!("must provide root port identifiers");
2243        }
2244
2245        let mut s = names.split(':');
2246        let rc_name = s.next().context("expected name of parent root complex")?;
2247        let rp_name = s.next().context("expected root port name")?;
2248
2249        if let Some(extra) = s.next() {
2250            anyhow::bail!("unexpected token: '{extra}'")
2251        }
2252
2253        let mut hotplug = false;
2254        let mut acs_capabilities_supported = None;
2255
2256        // Parse optional flags
2257        for opt in opts {
2258            let mut kv = opt.split('=');
2259            let key = kv.next().context("expected option name")?;
2260            let value = kv.next();
2261
2262            match key {
2263                "hotplug" => {
2264                    if value.is_some() {
2265                        anyhow::bail!("hotplug option does not take a value")
2266                    }
2267                    hotplug = true;
2268                }
2269                "acs" => {
2270                    let value = value.context("acs option requires a value")?;
2271                    if kv.next().is_some() {
2272                        anyhow::bail!("acs option expects a single value")
2273                    }
2274                    acs_capabilities_supported = Some(parse_acs_capability_mask(value)?);
2275                }
2276                _ => anyhow::bail!("unexpected option: '{opt}'"),
2277            }
2278        }
2279
2280        Ok(PcieRootPortCli {
2281            root_complex_name: rc_name.to_string(),
2282            name: rp_name.to_string(),
2283            hotplug,
2284            acs_capabilities_supported,
2285        })
2286    }
2287}
2288
2289#[derive(Clone, Debug, PartialEq)]
2290pub struct GenericPcieSwitchCli {
2291    pub port_name: String,
2292    pub name: String,
2293    pub num_downstream_ports: u8,
2294    pub hotplug: bool,
2295    pub acs_capabilities_supported: Option<u16>,
2296}
2297
2298impl FromStr for GenericPcieSwitchCli {
2299    type Err = anyhow::Error;
2300
2301    fn from_str(s: &str) -> Result<Self, Self::Err> {
2302        let mut opts = s.split(',');
2303        let names = opts.next().context("expected switch identifiers")?;
2304        if names.is_empty() {
2305            anyhow::bail!("must provide switch identifiers");
2306        }
2307
2308        let mut s = names.split(':');
2309        let port_name = s.next().context("expected name of parent port")?;
2310        let switch_name = s.next().context("expected switch name")?;
2311
2312        if let Some(extra) = s.next() {
2313            anyhow::bail!("unexpected token: '{extra}'")
2314        }
2315
2316        let mut num_downstream_ports = 4u8; // Default value
2317        let mut hotplug = false;
2318        let mut acs_capabilities_supported = None;
2319
2320        for opt in opts {
2321            let mut kv = opt.split('=');
2322            let key = kv.next().context("expected option name")?;
2323
2324            match key {
2325                "num_downstream_ports" => {
2326                    let value = kv.next().context("expected option value")?;
2327                    if let Some(extra) = kv.next() {
2328                        anyhow::bail!("unexpected token: '{extra}'")
2329                    }
2330                    num_downstream_ports = value.parse().context("invalid num_downstream_ports")?;
2331                }
2332                "hotplug" => {
2333                    if kv.next().is_some() {
2334                        anyhow::bail!("hotplug option does not take a value")
2335                    }
2336                    hotplug = true;
2337                }
2338                "acs" => {
2339                    let value = kv.next().context("acs option requires a value")?;
2340                    if kv.next().is_some() {
2341                        anyhow::bail!("acs option expects a single value")
2342                    }
2343                    acs_capabilities_supported = Some(parse_acs_capability_mask(value)?);
2344                }
2345                _ => anyhow::bail!("unknown option: '{key}'"),
2346            }
2347        }
2348
2349        Ok(GenericPcieSwitchCli {
2350            port_name: port_name.to_string(),
2351            name: switch_name.to_string(),
2352            num_downstream_ports,
2353            hotplug,
2354            acs_capabilities_supported,
2355        })
2356    }
2357}
2358
2359/// CLI configuration for a PCIe remote device.
2360#[derive(Clone, Debug, PartialEq)]
2361pub struct PcieRemoteCli {
2362    /// Name of the PCIe downstream port to attach to.
2363    pub port_name: String,
2364    /// TCP socket address for the remote simulator.
2365    pub socket_addr: Option<String>,
2366    /// Hardware unit identifier for plug request.
2367    pub hu: u16,
2368    /// Controller identifier for plug request.
2369    pub controller: u16,
2370}
2371
2372impl FromStr for PcieRemoteCli {
2373    type Err = anyhow::Error;
2374
2375    fn from_str(s: &str) -> Result<Self, Self::Err> {
2376        let mut opts = s.split(',');
2377        let port_name = opts.next().context("expected port name")?;
2378        if port_name.is_empty() {
2379            anyhow::bail!("must provide a port name");
2380        }
2381
2382        let mut socket_addr = None;
2383        let mut hu = 0u16;
2384        let mut controller = 0u16;
2385
2386        for opt in opts {
2387            let mut kv = opt.split('=');
2388            let key = kv.next().context("expected option name")?;
2389            let value = kv.next();
2390
2391            match key {
2392                "socket" => {
2393                    let addr = value.context("socket requires an address")?;
2394                    if let Some(extra) = kv.next() {
2395                        anyhow::bail!("unexpected token: '{extra}'")
2396                    }
2397                    if addr.is_empty() {
2398                        anyhow::bail!("socket address cannot be empty");
2399                    }
2400                    socket_addr = Some(addr.to_string());
2401                }
2402                "hu" => {
2403                    let val = value.context("hu requires a value")?;
2404                    if let Some(extra) = kv.next() {
2405                        anyhow::bail!("unexpected token: '{extra}'")
2406                    }
2407                    hu = val.parse().context("failed to parse hu")?;
2408                }
2409                "controller" => {
2410                    let val = value.context("controller requires a value")?;
2411                    if let Some(extra) = kv.next() {
2412                        anyhow::bail!("unexpected token: '{extra}'")
2413                    }
2414                    controller = val.parse().context("failed to parse controller")?;
2415                }
2416                _ => anyhow::bail!("unknown option: '{key}'"),
2417            }
2418        }
2419
2420        Ok(PcieRemoteCli {
2421            port_name: port_name.to_string(),
2422            socket_addr,
2423            hu,
2424            controller,
2425        })
2426    }
2427}
2428
2429/// CLI configuration for a VFIO-assigned PCI device.
2430#[cfg(target_os = "linux")]
2431#[derive(Clone, Debug)]
2432pub struct VfioDeviceCli {
2433    /// Name of the PCIe downstream port to attach to.
2434    pub port_name: String,
2435    /// PCI BDF address of the device on the host (e.g., "0000:01:00.0").
2436    pub pci_id: String,
2437}
2438
2439#[cfg(target_os = "linux")]
2440impl FromStr for VfioDeviceCli {
2441    type Err = anyhow::Error;
2442
2443    fn from_str(s: &str) -> Result<Self, Self::Err> {
2444        let (port_name, pci_id) = s
2445            .split_once(':')
2446            .context("expected <port_name>:<pci_bdf> (e.g., rp0:0000:01:00.0)")?;
2447
2448        if port_name.is_empty() {
2449            anyhow::bail!("port name cannot be empty");
2450        }
2451
2452        if pci_id.is_empty() {
2453            anyhow::bail!("PCI address cannot be empty");
2454        }
2455
2456        // Reject path separators to prevent sysfs path traversal via Path::join.
2457        if pci_id.contains('/') || pci_id.contains("..") {
2458            anyhow::bail!("PCI address must not contain path separators");
2459        }
2460
2461        Ok(VfioDeviceCli {
2462            port_name: port_name.to_string(),
2463            pci_id: pci_id.to_string(),
2464        })
2465    }
2466}
2467
2468/// Read a environment variable that may / may-not have a target-specific
2469/// prefix. e.g: `default_value_from_arch_env("FOO")` would first try and read
2470/// from `FOO`, and if that's not found, it will try `X86_64_FOO`.
2471///
2472/// Must return an `OsString`, in order to be compatible with `clap`'s
2473/// default_value code. As such - to encode the absence of the env-var, an empty
2474/// OsString is returned.
2475fn default_value_from_arch_env(name: &str) -> OsString {
2476    let prefix = if cfg!(guest_arch = "x86_64") {
2477        "X86_64"
2478    } else if cfg!(guest_arch = "aarch64") {
2479        "AARCH64"
2480    } else {
2481        return Default::default();
2482    };
2483    let prefixed = format!("{}_{}", prefix, name);
2484    std::env::var_os(name)
2485        .or_else(|| std::env::var_os(prefixed))
2486        .unwrap_or_default()
2487}
2488
2489/// Workaround to use `Option<PathBuf>` alongside [`default_value_from_arch_env`]
2490#[derive(Clone)]
2491pub struct OptionalPathBuf(pub Option<PathBuf>);
2492
2493impl From<&std::ffi::OsStr> for OptionalPathBuf {
2494    fn from(s: &std::ffi::OsStr) -> Self {
2495        OptionalPathBuf(if s.is_empty() { None } else { Some(s.into()) })
2496    }
2497}
2498
2499#[cfg(target_os = "linux")]
2500#[derive(Clone)]
2501pub enum VhostUserDeviceTypeCli {
2502    /// Block device — config from backend via GET_CONFIG, with num_queues
2503    /// patched by the frontend.
2504    Blk {
2505        num_queues: Option<u16>,
2506        queue_size: Option<u16>,
2507    },
2508    /// Filesystem device — frontend-owned config with mount tag.
2509    Fs {
2510        tag: String,
2511        num_queues: Option<u16>,
2512        queue_size: Option<u16>,
2513    },
2514    /// Generic device identified by numeric virtio device ID.
2515    Other {
2516        device_id: u16,
2517        queue_sizes: Vec<u16>,
2518    },
2519}
2520
2521#[cfg(target_os = "linux")]
2522#[derive(Clone)]
2523pub struct VhostUserCli {
2524    pub socket_path: String,
2525    pub device_type: VhostUserDeviceTypeCli,
2526    pub pcie_port: Option<String>,
2527}
2528
2529/// Split a string on commas, but not inside `[…]` brackets.
2530///
2531/// Returns an error on mismatched brackets (unmatched `]` or unclosed `[`).
2532#[cfg(target_os = "linux")]
2533fn split_respecting_brackets(s: &str) -> anyhow::Result<Vec<&str>> {
2534    let mut result = Vec::new();
2535    let mut start = 0;
2536    let mut depth: i32 = 0;
2537    for (i, c) in s.char_indices() {
2538        match c {
2539            '[' => depth += 1,
2540            ']' => {
2541                depth -= 1;
2542                anyhow::ensure!(depth >= 0, "unmatched ']' in option string");
2543            }
2544            ',' if depth == 0 => {
2545                result.push(&s[start..i]);
2546                start = i + 1;
2547            }
2548            _ => {}
2549        }
2550    }
2551    anyhow::ensure!(depth == 0, "unclosed '[' in option string");
2552    result.push(&s[start..]);
2553    Ok(result)
2554}
2555
2556#[cfg(target_os = "linux")]
2557impl FromStr for VhostUserCli {
2558    type Err = anyhow::Error;
2559
2560    fn from_str(s: &str) -> anyhow::Result<Self> {
2561        // Split on commas, but not inside brackets (for queue_sizes=[N,N]).
2562        let parts = split_respecting_brackets(s)?;
2563        let mut parts_iter = parts.into_iter();
2564        let socket_path = parts_iter
2565            .next()
2566            .context("missing socket path")?
2567            .to_string();
2568
2569        let mut device_id: Option<u16> = None;
2570        let mut tag: Option<String> = None;
2571        let mut pcie_port: Option<String> = None;
2572        let mut type_name = None;
2573        let mut num_queues: Option<u16> = None;
2574        let mut queue_size: Option<u16> = None;
2575        let mut queue_sizes: Option<Vec<u16>> = None;
2576        for opt in parts_iter {
2577            let (key, val) = opt.split_once('=').context("expected key=value option")?;
2578            match key {
2579                "type" => {
2580                    type_name = Some(val);
2581                }
2582                "device_id" => {
2583                    device_id = Some(val.parse().context("invalid device_id")?);
2584                }
2585                "tag" => {
2586                    tag = Some(val.to_string());
2587                }
2588                "pcie_port" => {
2589                    pcie_port = Some(val.to_string());
2590                }
2591                "num_queues" => {
2592                    num_queues = Some(val.parse().context("invalid num_queues")?);
2593                }
2594                "queue_size" => {
2595                    queue_size = Some(val.parse().context("invalid queue_size")?);
2596                }
2597                "queue_sizes" => {
2598                    // Parse bracket-delimited comma-separated list: [N,N,N]
2599                    let trimmed = val
2600                        .strip_prefix('[')
2601                        .and_then(|v| v.strip_suffix(']'))
2602                        .context("queue_sizes must be bracketed: [N,N,N]")?;
2603                    let sizes: Vec<u16> = trimmed
2604                        .split(',')
2605                        .map(|s| s.parse().context("invalid queue size in queue_sizes"))
2606                        .collect::<anyhow::Result<_>>()?;
2607                    anyhow::ensure!(!sizes.is_empty(), "queue_sizes must be non-empty");
2608                    queue_sizes = Some(sizes);
2609                }
2610                other => anyhow::bail!("unknown vhost-user option: '{other}'"),
2611            }
2612        }
2613
2614        if type_name.is_some() == device_id.is_some() {
2615            anyhow::bail!("must specify type=<name> or device_id=<N>");
2616        }
2617
2618        // Build the typed device variant.
2619        let device_type = match type_name {
2620            Some("fs") => {
2621                let tag = tag.take().context("type=fs requires tag=<name>")?;
2622                VhostUserDeviceTypeCli::Fs {
2623                    tag,
2624                    num_queues: num_queues.take(),
2625                    queue_size: queue_size.take(),
2626                }
2627            }
2628            Some("blk") => VhostUserDeviceTypeCli::Blk {
2629                num_queues: num_queues.take(),
2630                queue_size: queue_size.take(),
2631            },
2632            Some(ty) => anyhow::bail!("unknown vhost-user device type: '{ty}'"),
2633            None => {
2634                let queue_sizes = queue_sizes
2635                    .take()
2636                    .context("device_id= requires queue_sizes=[N,N,...]")?;
2637                VhostUserDeviceTypeCli::Other {
2638                    device_id: device_id.unwrap(),
2639                    queue_sizes,
2640                }
2641            }
2642        };
2643
2644        if tag.is_some() {
2645            anyhow::bail!("tag= is only valid for type=fs");
2646        }
2647        if queue_sizes.is_some() {
2648            anyhow::bail!("queue_sizes= is only valid for device_id=");
2649        }
2650        if num_queues.is_some() || queue_size.is_some() {
2651            anyhow::bail!(
2652                "num_queues= and queue_size= are not valid for device_id=; use queue_sizes="
2653            );
2654        }
2655
2656        Ok(VhostUserCli {
2657            socket_path,
2658            device_type,
2659            pcie_port,
2660        })
2661    }
2662}
2663
2664#[cfg(test)]
2665mod tests {
2666    use super::*;
2667
2668    use std::path::Path;
2669
2670    #[test]
2671    fn test_parse_file_opts() {
2672        // file: prefix with create
2673        let disk = DiskCliKind::from_str("file:test.vhd;create=1G").unwrap();
2674        assert!(matches!(
2675            &disk,
2676            DiskCliKind::File { path, create_with_len: Some(len), direct: false }
2677                if path == Path::new("test.vhd") && *len == 1024 * 1024 * 1024
2678        ));
2679
2680        // bare path with create (no file: prefix)
2681        let disk = DiskCliKind::from_str("test.vhd;create=1G").unwrap();
2682        assert!(matches!(
2683            &disk,
2684            DiskCliKind::File { path, create_with_len: Some(len), direct: false }
2685                if path == Path::new("test.vhd") && *len == 1024 * 1024 * 1024
2686        ));
2687
2688        // direct flag
2689        let disk = DiskCliKind::from_str("file:/dev/sdb;direct").unwrap();
2690        assert!(matches!(
2691            &disk,
2692            DiskCliKind::File { path, create_with_len: None, direct: true }
2693                if path == Path::new("/dev/sdb")
2694        ));
2695
2696        // direct + create in either order
2697        let disk = DiskCliKind::from_str("file:disk.img;direct;create=1G").unwrap();
2698        assert!(matches!(
2699            &disk,
2700            DiskCliKind::File { path, create_with_len: Some(len), direct: true }
2701                if path == Path::new("disk.img") && *len == 1024 * 1024 * 1024
2702        ));
2703
2704        let disk = DiskCliKind::from_str("file:disk.img;create=1G;direct").unwrap();
2705        assert!(matches!(
2706            &disk,
2707            DiskCliKind::File { path, create_with_len: Some(len), direct: true }
2708                if path == Path::new("disk.img") && *len == 1024 * 1024 * 1024
2709        ));
2710
2711        // plain path, no options
2712        let disk = DiskCliKind::from_str("file:disk.img").unwrap();
2713        assert!(matches!(
2714            &disk,
2715            DiskCliKind::File { path, create_with_len: None, direct: false }
2716                if path == Path::new("disk.img")
2717        ));
2718
2719        // invalid option rejected
2720        assert!(DiskCliKind::from_str("file:disk.img;bogus").is_err());
2721
2722        // direct rejected for sql disks
2723        assert!(DiskCliKind::from_str("sql:db.sqlite;direct").is_err());
2724    }
2725
2726    #[test]
2727    fn test_parse_memory_disk() {
2728        let s = "mem:1G";
2729        let disk = DiskCliKind::from_str(s).unwrap();
2730        match disk {
2731            DiskCliKind::Memory(size) => {
2732                assert_eq!(size, 1024 * 1024 * 1024); // 1G
2733            }
2734            _ => panic!("Expected Memory variant"),
2735        }
2736    }
2737
2738    #[test]
2739    fn test_parse_pcie_disk() {
2740        assert_eq!(
2741            DiskCli::from_str("mem:1G,pcie_port=p0").unwrap().pcie_port,
2742            Some("p0".to_string())
2743        );
2744        assert_eq!(
2745            DiskCli::from_str("file:path.vhdx,pcie_port=p0")
2746                .unwrap()
2747                .pcie_port,
2748            Some("p0".to_string())
2749        );
2750        assert_eq!(
2751            DiskCli::from_str("memdiff:file:path.vhdx,pcie_port=p0")
2752                .unwrap()
2753                .pcie_port,
2754            Some("p0".to_string())
2755        );
2756
2757        // Missing port name
2758        assert!(DiskCli::from_str("file:disk.vhd,pcie_port=").is_err());
2759
2760        // Incompatible with various other disk fields
2761        assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,vtl2").is_err());
2762        assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,uh").is_err());
2763        assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,uh-nvme").is_err());
2764    }
2765
2766    #[test]
2767    fn test_parse_memory_diff_disk() {
2768        let s = "memdiff:file:base.img";
2769        let disk = DiskCliKind::from_str(s).unwrap();
2770        match disk {
2771            DiskCliKind::MemoryDiff(inner) => match *inner {
2772                DiskCliKind::File {
2773                    path,
2774                    create_with_len,
2775                    ..
2776                } => {
2777                    assert_eq!(path, PathBuf::from("base.img"));
2778                    assert_eq!(create_with_len, None);
2779                }
2780                _ => panic!("Expected File variant inside MemoryDiff"),
2781            },
2782            _ => panic!("Expected MemoryDiff variant"),
2783        }
2784    }
2785
2786    #[test]
2787    fn test_parse_sqlite_disk() {
2788        let s = "sql:db.sqlite;create=2G";
2789        let disk = DiskCliKind::from_str(s).unwrap();
2790        match disk {
2791            DiskCliKind::Sqlite {
2792                path,
2793                create_with_len,
2794            } => {
2795                assert_eq!(path, PathBuf::from("db.sqlite"));
2796                assert_eq!(create_with_len, Some(2 * 1024 * 1024 * 1024));
2797            }
2798            _ => panic!("Expected Sqlite variant"),
2799        }
2800
2801        // Test without create option
2802        let s = "sql:db.sqlite";
2803        let disk = DiskCliKind::from_str(s).unwrap();
2804        match disk {
2805            DiskCliKind::Sqlite {
2806                path,
2807                create_with_len,
2808            } => {
2809                assert_eq!(path, PathBuf::from("db.sqlite"));
2810                assert_eq!(create_with_len, None);
2811            }
2812            _ => panic!("Expected Sqlite variant"),
2813        }
2814    }
2815
2816    #[test]
2817    fn test_parse_sqlite_diff_disk() {
2818        // Test with create option
2819        let s = "sqldiff:diff.sqlite;create:file:base.img";
2820        let disk = DiskCliKind::from_str(s).unwrap();
2821        match disk {
2822            DiskCliKind::SqliteDiff { path, create, disk } => {
2823                assert_eq!(path, PathBuf::from("diff.sqlite"));
2824                assert!(create);
2825                match *disk {
2826                    DiskCliKind::File {
2827                        path,
2828                        create_with_len,
2829                        ..
2830                    } => {
2831                        assert_eq!(path, PathBuf::from("base.img"));
2832                        assert_eq!(create_with_len, None);
2833                    }
2834                    _ => panic!("Expected File variant inside SqliteDiff"),
2835                }
2836            }
2837            _ => panic!("Expected SqliteDiff variant"),
2838        }
2839
2840        // Test without create option
2841        let s = "sqldiff:diff.sqlite:file:base.img";
2842        let disk = DiskCliKind::from_str(s).unwrap();
2843        match disk {
2844            DiskCliKind::SqliteDiff { path, create, disk } => {
2845                assert_eq!(path, PathBuf::from("diff.sqlite"));
2846                assert!(!create);
2847                match *disk {
2848                    DiskCliKind::File {
2849                        path,
2850                        create_with_len,
2851                        ..
2852                    } => {
2853                        assert_eq!(path, PathBuf::from("base.img"));
2854                        assert_eq!(create_with_len, None);
2855                    }
2856                    _ => panic!("Expected File variant inside SqliteDiff"),
2857                }
2858            }
2859            _ => panic!("Expected SqliteDiff variant"),
2860        }
2861    }
2862
2863    #[test]
2864    fn test_parse_autocache_sqlite_disk() {
2865        // Test with cache path provided
2866        let disk =
2867            DiskCliKind::parse_autocache(":file:disk.vhd", Ok("/tmp/cache".to_string())).unwrap();
2868        assert!(matches!(
2869            disk,
2870            DiskCliKind::AutoCacheSqlite {
2871                cache_path,
2872                key,
2873                disk: _disk,
2874            } if cache_path == "/tmp/cache" && key.is_none()
2875        ));
2876
2877        // Test with key
2878        let disk =
2879            DiskCliKind::parse_autocache("mykey:file:disk.vhd", Ok("/tmp/cache".to_string()))
2880                .unwrap();
2881        assert!(matches!(
2882            disk,
2883            DiskCliKind::AutoCacheSqlite {
2884                cache_path,
2885                key: Some(key),
2886                disk: _disk,
2887            } if cache_path == "/tmp/cache" && key == "mykey"
2888        ));
2889
2890        // Test without cache path
2891        assert!(
2892            DiskCliKind::parse_autocache(":file:disk.vhd", Err(std::env::VarError::NotPresent),)
2893                .is_err()
2894        );
2895    }
2896
2897    #[test]
2898    fn test_parse_disk_errors() {
2899        assert!(DiskCliKind::from_str("invalid:").is_err());
2900        assert!(DiskCliKind::from_str("memory:extra").is_err());
2901
2902        // Test sqlite: without environment variable
2903        assert!(DiskCliKind::from_str("sqlite:").is_err());
2904    }
2905
2906    #[test]
2907    fn test_parse_errors() {
2908        // Invalid memory size
2909        assert!(DiskCliKind::from_str("mem:invalid").is_err());
2910
2911        // Invalid syntax for SQLiteDiff
2912        assert!(DiskCliKind::from_str("sqldiff:path").is_err());
2913
2914        // Missing OPENVMM_AUTO_CACHE_PATH for AutoCacheSqlite
2915        assert!(
2916            DiskCliKind::parse_autocache("key:file:disk.vhd", Err(std::env::VarError::NotPresent),)
2917                .is_err()
2918        );
2919
2920        // Invalid blob kind
2921        assert!(DiskCliKind::from_str("blob:invalid:url").is_err());
2922
2923        // Invalid cipher
2924        assert!(DiskCliKind::from_str("crypt:invalid:key.bin:file:disk.vhd").is_err());
2925
2926        // Invalid format for crypt (missing parts)
2927        assert!(DiskCliKind::from_str("crypt:xts-aes-256:key.bin").is_err());
2928
2929        // Invalid disk kind
2930        assert!(DiskCliKind::from_str("invalid:path").is_err());
2931
2932        // Missing create size
2933        assert!(DiskCliKind::from_str("file:disk.vhd;create=").is_err());
2934    }
2935
2936    #[test]
2937    fn test_fs_args_from_str() {
2938        let args = FsArgs::from_str("tag1,/path/to/fs").unwrap();
2939        assert_eq!(args.tag, "tag1");
2940        assert_eq!(args.path, "/path/to/fs");
2941
2942        // Test error cases
2943        assert!(FsArgs::from_str("tag1").is_err());
2944        assert!(FsArgs::from_str("tag1,/path,extra").is_err());
2945    }
2946
2947    #[test]
2948    fn test_fs_args_with_options_from_str() {
2949        let args = FsArgsWithOptions::from_str("tag1,/path/to/fs,opt1,opt2").unwrap();
2950        assert_eq!(args.tag, "tag1");
2951        assert_eq!(args.path, "/path/to/fs");
2952        assert_eq!(args.options, "opt1;opt2");
2953
2954        // Test without options
2955        let args = FsArgsWithOptions::from_str("tag1,/path/to/fs").unwrap();
2956        assert_eq!(args.tag, "tag1");
2957        assert_eq!(args.path, "/path/to/fs");
2958        assert_eq!(args.options, "");
2959
2960        // Test error case
2961        assert!(FsArgsWithOptions::from_str("tag1").is_err());
2962    }
2963
2964    #[test]
2965    fn test_serial_config_from_str() {
2966        assert_eq!(
2967            SerialConfigCli::from_str("none").unwrap(),
2968            SerialConfigCli::None
2969        );
2970        assert_eq!(
2971            SerialConfigCli::from_str("console").unwrap(),
2972            SerialConfigCli::Console
2973        );
2974        assert_eq!(
2975            SerialConfigCli::from_str("stderr").unwrap(),
2976            SerialConfigCli::Stderr
2977        );
2978
2979        // Test file config
2980        let file_config = SerialConfigCli::from_str("file=/path/to/file").unwrap();
2981        if let SerialConfigCli::File(path) = file_config {
2982            assert_eq!(path.to_str().unwrap(), "/path/to/file");
2983        } else {
2984            panic!("Expected File variant");
2985        }
2986
2987        // Test term config with name, but no specific path
2988        match SerialConfigCli::from_str("term,name=MyTerm").unwrap() {
2989            SerialConfigCli::NewConsole(None, Some(name)) => {
2990                assert_eq!(name, "MyTerm");
2991            }
2992            _ => panic!("Expected NewConsole variant with name"),
2993        }
2994
2995        // Test term config without name, but no specific path
2996        match SerialConfigCli::from_str("term").unwrap() {
2997            SerialConfigCli::NewConsole(None, None) => (),
2998            _ => panic!("Expected NewConsole variant without name"),
2999        }
3000
3001        // Test term config with name
3002        match SerialConfigCli::from_str("term=/dev/pts/0,name=MyTerm").unwrap() {
3003            SerialConfigCli::NewConsole(Some(path), Some(name)) => {
3004                assert_eq!(path.to_str().unwrap(), "/dev/pts/0");
3005                assert_eq!(name, "MyTerm");
3006            }
3007            _ => panic!("Expected NewConsole variant with name"),
3008        }
3009
3010        // Test term config without name
3011        match SerialConfigCli::from_str("term=/dev/pts/0").unwrap() {
3012            SerialConfigCli::NewConsole(Some(path), None) => {
3013                assert_eq!(path.to_str().unwrap(), "/dev/pts/0");
3014            }
3015            _ => panic!("Expected NewConsole variant without name"),
3016        }
3017
3018        // Test TCP config
3019        match SerialConfigCli::from_str("listen=tcp:127.0.0.1:1234").unwrap() {
3020            SerialConfigCli::Tcp(addr) => {
3021                assert_eq!(addr.to_string(), "127.0.0.1:1234");
3022            }
3023            _ => panic!("Expected Tcp variant"),
3024        }
3025
3026        // Test pipe config
3027        match SerialConfigCli::from_str("listen=/path/to/pipe").unwrap() {
3028            SerialConfigCli::Pipe(path) => {
3029                assert_eq!(path.to_str().unwrap(), "/path/to/pipe");
3030            }
3031            _ => panic!("Expected Pipe variant"),
3032        }
3033
3034        // Test error cases
3035        assert!(SerialConfigCli::from_str("").is_err());
3036        assert!(SerialConfigCli::from_str("unknown").is_err());
3037        assert!(SerialConfigCli::from_str("file").is_err());
3038        assert!(SerialConfigCli::from_str("listen").is_err());
3039    }
3040
3041    #[test]
3042    fn test_endpoint_config_from_str() {
3043        // Test none
3044        assert!(matches!(
3045            EndpointConfigCli::from_str("none").unwrap(),
3046            EndpointConfigCli::None
3047        ));
3048
3049        // Test consomme without cidr
3050        match EndpointConfigCli::from_str("consomme").unwrap() {
3051            EndpointConfigCli::Consomme {
3052                cidr: None,
3053                host_fwd,
3054            } => assert!(host_fwd.is_empty()),
3055            _ => panic!("Expected Consomme variant without cidr"),
3056        }
3057
3058        // Test consomme with cidr
3059        match EndpointConfigCli::from_str("consomme:192.168.0.0/24").unwrap() {
3060            EndpointConfigCli::Consomme {
3061                cidr: Some(cidr),
3062                host_fwd,
3063            } => {
3064                assert_eq!(cidr, "192.168.0.0/24");
3065                assert!(host_fwd.is_empty());
3066            }
3067            _ => panic!("Expected Consomme variant with cidr"),
3068        }
3069
3070        // Test consomme with hostfwd
3071        match EndpointConfigCli::from_str("consomme:hostfwd=udp:127.0.0.1:5000-:5000").unwrap() {
3072            EndpointConfigCli::Consomme { cidr, host_fwd } => {
3073                assert!(cidr.is_none());
3074                assert_eq!(host_fwd.len(), 1);
3075                assert_eq!(host_fwd[0].protocol, HostPortProtocolCli::Udp);
3076                assert_eq!(
3077                    host_fwd[0].host_address,
3078                    Some(std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)))
3079                );
3080                assert_eq!(host_fwd[0].host_port, 5000);
3081                assert_eq!(host_fwd[0].guest_port, 5000);
3082            }
3083            _ => panic!("Expected Consomme variant with hostfwd"),
3084        }
3085
3086        // Test consomme with cidr and hostfwd
3087        match EndpointConfigCli::from_str("consomme:10.0.0.0/24,hostfwd=tcp::2222-:22").unwrap() {
3088            EndpointConfigCli::Consomme { cidr, host_fwd } => {
3089                assert_eq!(cidr.as_deref(), Some("10.0.0.0/24"));
3090                assert_eq!(host_fwd.len(), 1);
3091                assert_eq!(host_fwd[0].protocol, HostPortProtocolCli::Tcp);
3092                assert_eq!(host_fwd[0].host_port, 2222);
3093                assert_eq!(host_fwd[0].guest_port, 22);
3094            }
3095            _ => panic!("Expected Consomme variant with cidr and hostfwd"),
3096        }
3097
3098        // Test consomme with multiple hostfwd
3099        match EndpointConfigCli::from_str("consomme:hostfwd=tcp::2222-:22,hostfwd=tcp::3389-:3389")
3100            .unwrap()
3101        {
3102            EndpointConfigCli::Consomme { cidr, host_fwd } => {
3103                assert!(cidr.is_none());
3104                assert_eq!(host_fwd.len(), 2);
3105                assert_eq!(host_fwd[0].host_port, 2222);
3106                assert_eq!(host_fwd[0].guest_port, 22);
3107                assert_eq!(host_fwd[1].host_port, 3389);
3108                assert_eq!(host_fwd[1].guest_port, 3389);
3109            }
3110            _ => panic!("Expected Consomme variant with multiple hostfwd"),
3111        }
3112
3113        // Test consomme with different host and guest ports
3114        match EndpointConfigCli::from_str("consomme:hostfwd=tcp:127.0.0.1:8080-:80").unwrap() {
3115            EndpointConfigCli::Consomme { cidr, host_fwd } => {
3116                assert!(cidr.is_none());
3117                assert_eq!(host_fwd.len(), 1);
3118                assert_eq!(host_fwd[0].protocol, HostPortProtocolCli::Tcp);
3119                assert_eq!(
3120                    host_fwd[0].host_address,
3121                    Some(std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)))
3122                );
3123                assert_eq!(host_fwd[0].host_port, 8080);
3124                assert_eq!(host_fwd[0].guest_port, 80);
3125            }
3126            _ => panic!("Expected Consomme variant with host/guest port mapping"),
3127        }
3128
3129        // Test consomme with guest address (accepted but ignored by backend)
3130        match EndpointConfigCli::from_str("consomme:hostfwd=tcp::8080-10.0.0.2:80").unwrap() {
3131            EndpointConfigCli::Consomme { cidr, host_fwd } => {
3132                assert!(cidr.is_none());
3133                assert_eq!(host_fwd[0].host_port, 8080);
3134                assert_eq!(host_fwd[0].guest_port, 80);
3135            }
3136            _ => panic!("Expected Consomme variant with guest address"),
3137        }
3138
3139        // Test consomme with IPv6 host address (bracketed)
3140        match EndpointConfigCli::from_str("consomme:hostfwd=tcp:[::1]:8080-:80").unwrap() {
3141            EndpointConfigCli::Consomme { cidr, host_fwd } => {
3142                assert!(cidr.is_none());
3143                assert_eq!(host_fwd.len(), 1);
3144                assert_eq!(host_fwd[0].protocol, HostPortProtocolCli::Tcp);
3145                assert_eq!(
3146                    host_fwd[0].host_address,
3147                    Some(std::net::IpAddr::V6(std::net::Ipv6Addr::LOCALHOST))
3148                );
3149                assert_eq!(host_fwd[0].host_port, 8080);
3150                assert_eq!(host_fwd[0].guest_port, 80);
3151            }
3152            _ => panic!("Expected Consomme variant with IPv6 hostfwd"),
3153        }
3154
3155        // Test consomme with IPv6 guest address (bracketed)
3156        match EndpointConfigCli::from_str("consomme:hostfwd=tcp::8080-[::1]:80").unwrap() {
3157            EndpointConfigCli::Consomme { cidr, host_fwd } => {
3158                assert!(cidr.is_none());
3159                assert_eq!(host_fwd[0].host_port, 8080);
3160                assert_eq!(host_fwd[0].guest_port, 80);
3161            }
3162            _ => panic!("Expected Consomme variant with IPv6 guest address"),
3163        }
3164
3165        // Test dio without id
3166        match EndpointConfigCli::from_str("dio").unwrap() {
3167            EndpointConfigCli::Dio { id: None } => (),
3168            _ => panic!("Expected Dio variant without id"),
3169        }
3170
3171        // Test dio with id
3172        match EndpointConfigCli::from_str("dio:test_id").unwrap() {
3173            EndpointConfigCli::Dio { id: Some(id) } => {
3174                assert_eq!(id, "test_id");
3175            }
3176            _ => panic!("Expected Dio variant with id"),
3177        }
3178
3179        // Test tap
3180        match EndpointConfigCli::from_str("tap:tap0").unwrap() {
3181            EndpointConfigCli::Tap { name } => {
3182                assert_eq!(name, "tap0");
3183            }
3184            _ => panic!("Expected Tap variant"),
3185        }
3186
3187        // Test error case
3188        assert!(EndpointConfigCli::from_str("invalid").is_err());
3189    }
3190
3191    #[test]
3192    fn test_nic_config_from_str() {
3193        use openvmm_defs::config::DeviceVtl;
3194
3195        // Test basic endpoint
3196        let config = NicConfigCli::from_str("none").unwrap();
3197        assert_eq!(config.vtl, DeviceVtl::Vtl0);
3198        assert!(config.max_queues.is_none());
3199        assert!(!config.underhill);
3200        assert!(config.pcie_port.is_none());
3201        assert!(matches!(config.endpoint, EndpointConfigCli::None));
3202
3203        // Test with vtl2
3204        let config = NicConfigCli::from_str("vtl2:none").unwrap();
3205        assert_eq!(config.vtl, DeviceVtl::Vtl2);
3206        assert!(config.pcie_port.is_none());
3207        assert!(matches!(config.endpoint, EndpointConfigCli::None));
3208
3209        // Test with queues
3210        let config = NicConfigCli::from_str("queues=4:none").unwrap();
3211        assert_eq!(config.max_queues, Some(4));
3212        assert!(config.pcie_port.is_none());
3213        assert!(matches!(config.endpoint, EndpointConfigCli::None));
3214
3215        // Test with underhill
3216        let config = NicConfigCli::from_str("uh:none").unwrap();
3217        assert!(config.underhill);
3218        assert!(config.pcie_port.is_none());
3219        assert!(matches!(config.endpoint, EndpointConfigCli::None));
3220
3221        // Test with pcie_port
3222        let config = NicConfigCli::from_str("pcie_port=rp0:none").unwrap();
3223        assert_eq!(config.pcie_port.unwrap(), "rp0".to_string());
3224        assert!(matches!(config.endpoint, EndpointConfigCli::None));
3225
3226        // Test error cases
3227        assert!(NicConfigCli::from_str("queues=invalid:none").is_err());
3228        assert!(NicConfigCli::from_str("uh:vtl2:none").is_err()); // uh incompatible with vtl2
3229        assert!(NicConfigCli::from_str("pcie_port=rp0:vtl2:none").is_err());
3230        assert!(NicConfigCli::from_str("uh:pcie_port=rp0:none").is_err());
3231        assert!(NicConfigCli::from_str("pcie_port=:none").is_err());
3232        assert!(NicConfigCli::from_str("pcie_port:none").is_err());
3233    }
3234
3235    #[test]
3236    fn test_parse_pcie_port_prefix() {
3237        // Successful prefix parsing
3238        let (port, rest) = parse_pcie_port_prefix("pcie_port=rp0:tag,path");
3239        assert_eq!(port.unwrap(), "rp0");
3240        assert_eq!(rest, "tag,path");
3241
3242        // No prefix
3243        let (port, rest) = parse_pcie_port_prefix("tag,path");
3244        assert!(port.is_none());
3245        assert_eq!(rest, "tag,path");
3246
3247        // Empty port name — not parsed as a prefix
3248        let (port, rest) = parse_pcie_port_prefix("pcie_port=:tag,path");
3249        assert!(port.is_none());
3250        assert_eq!(rest, "pcie_port=:tag,path");
3251
3252        // Missing colon — not parsed as a prefix
3253        let (port, rest) = parse_pcie_port_prefix("pcie_port=rp0");
3254        assert!(port.is_none());
3255        assert_eq!(rest, "pcie_port=rp0");
3256    }
3257
3258    #[test]
3259    fn test_fs_args_pcie_port() {
3260        // Without pcie_port
3261        let args = FsArgs::from_str("myfs,/path").unwrap();
3262        assert_eq!(args.tag, "myfs");
3263        assert_eq!(args.path, "/path");
3264        assert!(args.pcie_port.is_none());
3265
3266        // With pcie_port
3267        let args = FsArgs::from_str("pcie_port=rp0:myfs,/path").unwrap();
3268        assert_eq!(args.pcie_port.unwrap(), "rp0");
3269        assert_eq!(args.tag, "myfs");
3270        assert_eq!(args.path, "/path");
3271
3272        // Error: wrong number of fields
3273        assert!(FsArgs::from_str("myfs").is_err());
3274        assert!(FsArgs::from_str("pcie_port=rp0:myfs").is_err());
3275    }
3276
3277    #[test]
3278    fn test_fs_args_with_options_pcie_port() {
3279        // Without pcie_port
3280        let args = FsArgsWithOptions::from_str("myfs,/path,uid=1000").unwrap();
3281        assert_eq!(args.tag, "myfs");
3282        assert_eq!(args.path, "/path");
3283        assert_eq!(args.options, "uid=1000");
3284        assert!(args.pcie_port.is_none());
3285
3286        // With pcie_port
3287        let args = FsArgsWithOptions::from_str("pcie_port=rp0:myfs,/path,uid=1000").unwrap();
3288        assert_eq!(args.pcie_port.unwrap(), "rp0");
3289        assert_eq!(args.tag, "myfs");
3290        assert_eq!(args.path, "/path");
3291        assert_eq!(args.options, "uid=1000");
3292
3293        // Error: missing path
3294        assert!(FsArgsWithOptions::from_str("myfs").is_err());
3295    }
3296
3297    #[test]
3298    fn test_virtio_pmem_args_pcie_port() {
3299        // Without pcie_port
3300        let args = VirtioPmemArgs::from_str("/path/to/file").unwrap();
3301        assert_eq!(args.path, "/path/to/file");
3302        assert!(args.pcie_port.is_none());
3303
3304        // With pcie_port
3305        let args = VirtioPmemArgs::from_str("pcie_port=rp0:/path/to/file").unwrap();
3306        assert_eq!(args.pcie_port.unwrap(), "rp0");
3307        assert_eq!(args.path, "/path/to/file");
3308
3309        // Error: empty path
3310        assert!(VirtioPmemArgs::from_str("").is_err());
3311        assert!(VirtioPmemArgs::from_str("pcie_port=rp0:").is_err());
3312    }
3313
3314    #[test]
3315    fn test_smt_config_from_str() {
3316        assert_eq!(SmtConfigCli::from_str("auto").unwrap(), SmtConfigCli::Auto);
3317        assert_eq!(
3318            SmtConfigCli::from_str("force").unwrap(),
3319            SmtConfigCli::Force
3320        );
3321        assert_eq!(SmtConfigCli::from_str("off").unwrap(), SmtConfigCli::Off);
3322
3323        // Test error cases
3324        assert!(SmtConfigCli::from_str("invalid").is_err());
3325        assert!(SmtConfigCli::from_str("").is_err());
3326    }
3327
3328    #[test]
3329    fn test_pcat_boot_order_from_str() {
3330        // Test single device
3331        let order = PcatBootOrderCli::from_str("optical").unwrap();
3332        assert_eq!(order.0[0], PcatBootDevice::Optical);
3333
3334        // Test multiple devices
3335        let order = PcatBootOrderCli::from_str("hdd,net").unwrap();
3336        assert_eq!(order.0[0], PcatBootDevice::HardDrive);
3337        assert_eq!(order.0[1], PcatBootDevice::Network);
3338
3339        // Test error cases
3340        assert!(PcatBootOrderCli::from_str("invalid").is_err());
3341        assert!(PcatBootOrderCli::from_str("optical,optical").is_err()); // duplicate device
3342    }
3343
3344    #[test]
3345    fn test_floppy_disk_from_str() {
3346        // Test basic disk
3347        let disk = FloppyDiskCli::from_str("file:/path/to/floppy.img").unwrap();
3348        assert!(!disk.read_only);
3349        match disk.kind {
3350            DiskCliKind::File {
3351                path,
3352                create_with_len,
3353                ..
3354            } => {
3355                assert_eq!(path.to_str().unwrap(), "/path/to/floppy.img");
3356                assert_eq!(create_with_len, None);
3357            }
3358            _ => panic!("Expected File variant"),
3359        }
3360
3361        // Test with read-only flag
3362        let disk = FloppyDiskCli::from_str("file:/path/to/floppy.img,ro").unwrap();
3363        assert!(disk.read_only);
3364
3365        // Test error cases
3366        assert!(FloppyDiskCli::from_str("").is_err());
3367        assert!(FloppyDiskCli::from_str("file:/path/to/floppy.img,invalid").is_err());
3368    }
3369
3370    #[test]
3371    fn test_pcie_root_complex_from_str() {
3372        const ONE_MB: u64 = 1024 * 1024;
3373        const ONE_GB: u64 = 1024 * ONE_MB;
3374
3375        const DEFAULT_LOW_MMIO: u32 = (64 * ONE_MB) as u32;
3376        const DEFAULT_HIGH_MMIO: u64 = ONE_GB;
3377
3378        assert_eq!(
3379            PcieRootComplexCli::from_str("rc0").unwrap(),
3380            PcieRootComplexCli {
3381                name: "rc0".to_string(),
3382                segment: 0,
3383                start_bus: 0,
3384                end_bus: 255,
3385                low_mmio: DEFAULT_LOW_MMIO,
3386                high_mmio: DEFAULT_HIGH_MMIO,
3387            }
3388        );
3389
3390        assert_eq!(
3391            PcieRootComplexCli::from_str("rc1,segment=1").unwrap(),
3392            PcieRootComplexCli {
3393                name: "rc1".to_string(),
3394                segment: 1,
3395                start_bus: 0,
3396                end_bus: 255,
3397                low_mmio: DEFAULT_LOW_MMIO,
3398                high_mmio: DEFAULT_HIGH_MMIO,
3399            }
3400        );
3401
3402        assert_eq!(
3403            PcieRootComplexCli::from_str("rc2,start_bus=32").unwrap(),
3404            PcieRootComplexCli {
3405                name: "rc2".to_string(),
3406                segment: 0,
3407                start_bus: 32,
3408                end_bus: 255,
3409                low_mmio: DEFAULT_LOW_MMIO,
3410                high_mmio: DEFAULT_HIGH_MMIO,
3411            }
3412        );
3413
3414        assert_eq!(
3415            PcieRootComplexCli::from_str("rc3,end_bus=31").unwrap(),
3416            PcieRootComplexCli {
3417                name: "rc3".to_string(),
3418                segment: 0,
3419                start_bus: 0,
3420                end_bus: 31,
3421                low_mmio: DEFAULT_LOW_MMIO,
3422                high_mmio: DEFAULT_HIGH_MMIO,
3423            }
3424        );
3425
3426        assert_eq!(
3427            PcieRootComplexCli::from_str("rc4,start_bus=32,end_bus=127,high_mmio=2G").unwrap(),
3428            PcieRootComplexCli {
3429                name: "rc4".to_string(),
3430                segment: 0,
3431                start_bus: 32,
3432                end_bus: 127,
3433                low_mmio: DEFAULT_LOW_MMIO,
3434                high_mmio: 2 * ONE_GB,
3435            }
3436        );
3437
3438        assert_eq!(
3439            PcieRootComplexCli::from_str("rc5,segment=2,start_bus=32,end_bus=127").unwrap(),
3440            PcieRootComplexCli {
3441                name: "rc5".to_string(),
3442                segment: 2,
3443                start_bus: 32,
3444                end_bus: 127,
3445                low_mmio: DEFAULT_LOW_MMIO,
3446                high_mmio: DEFAULT_HIGH_MMIO,
3447            }
3448        );
3449
3450        assert_eq!(
3451            PcieRootComplexCli::from_str("rc6,low_mmio=1M,high_mmio=64G").unwrap(),
3452            PcieRootComplexCli {
3453                name: "rc6".to_string(),
3454                segment: 0,
3455                start_bus: 0,
3456                end_bus: 255,
3457                low_mmio: ONE_MB as u32,
3458                high_mmio: 64 * ONE_GB,
3459            }
3460        );
3461
3462        // Error cases
3463        assert!(PcieRootComplexCli::from_str("").is_err());
3464        assert!(PcieRootComplexCli::from_str("poorly,").is_err());
3465        assert!(PcieRootComplexCli::from_str("configured,complex").is_err());
3466        assert!(PcieRootComplexCli::from_str("fails,start_bus=foo").is_err());
3467        assert!(PcieRootComplexCli::from_str("fails,start_bus=32,end_bus=31").is_err());
3468        assert!(PcieRootComplexCli::from_str("rc,start_bus=256").is_err());
3469        assert!(PcieRootComplexCli::from_str("rc,end_bus=256").is_err());
3470        assert!(PcieRootComplexCli::from_str("rc,low_mmio=5G").is_err());
3471        assert!(PcieRootComplexCli::from_str("rc,low_mmio=aG").is_err());
3472        assert!(PcieRootComplexCli::from_str("rc,high_mmio=bad").is_err());
3473        assert!(PcieRootComplexCli::from_str("rc,high_mmio").is_err());
3474    }
3475
3476    #[test]
3477    fn test_pcie_root_port_from_str() {
3478        assert_eq!(
3479            PcieRootPortCli::from_str("rc0:rc0rp0").unwrap(),
3480            PcieRootPortCli {
3481                root_complex_name: "rc0".to_string(),
3482                name: "rc0rp0".to_string(),
3483                hotplug: false,
3484                acs_capabilities_supported: None,
3485            }
3486        );
3487
3488        assert_eq!(
3489            PcieRootPortCli::from_str("my_rc:port2").unwrap(),
3490            PcieRootPortCli {
3491                root_complex_name: "my_rc".to_string(),
3492                name: "port2".to_string(),
3493                hotplug: false,
3494                acs_capabilities_supported: None,
3495            }
3496        );
3497
3498        // Test with hotplug flag
3499        assert_eq!(
3500            PcieRootPortCli::from_str("my_rc:port2,hotplug").unwrap(),
3501            PcieRootPortCli {
3502                root_complex_name: "my_rc".to_string(),
3503                name: "port2".to_string(),
3504                hotplug: true,
3505                acs_capabilities_supported: None,
3506            }
3507        );
3508
3509        assert_eq!(
3510            PcieRootPortCli::from_str("my_rc:port3,acs=0").unwrap(),
3511            PcieRootPortCli {
3512                root_complex_name: "my_rc".to_string(),
3513                name: "port3".to_string(),
3514                hotplug: false,
3515                acs_capabilities_supported: Some(0),
3516            }
3517        );
3518
3519        assert_eq!(
3520            PcieRootPortCli::from_str("my_rc:port3,acs=0x5f").unwrap(),
3521            PcieRootPortCli {
3522                root_complex_name: "my_rc".to_string(),
3523                name: "port3".to_string(),
3524                hotplug: false,
3525                acs_capabilities_supported: Some(0x005f),
3526            }
3527        );
3528
3529        // Error cases
3530        assert!(PcieRootPortCli::from_str("").is_err());
3531        assert!(PcieRootPortCli::from_str("rp0").is_err());
3532        assert!(PcieRootPortCli::from_str("rp0,opt").is_err());
3533        assert!(PcieRootPortCli::from_str("rc0:rp0:rp3").is_err());
3534        assert!(PcieRootPortCli::from_str("rc0:rp0,invalid_option").is_err());
3535    }
3536
3537    #[test]
3538    fn test_pcie_switch_from_str() {
3539        assert_eq!(
3540            GenericPcieSwitchCli::from_str("rp0:switch0").unwrap(),
3541            GenericPcieSwitchCli {
3542                port_name: "rp0".to_string(),
3543                name: "switch0".to_string(),
3544                num_downstream_ports: 4,
3545                hotplug: false,
3546                acs_capabilities_supported: None,
3547            }
3548        );
3549
3550        assert_eq!(
3551            GenericPcieSwitchCli::from_str("port1:my_switch,num_downstream_ports=4").unwrap(),
3552            GenericPcieSwitchCli {
3553                port_name: "port1".to_string(),
3554                name: "my_switch".to_string(),
3555                num_downstream_ports: 4,
3556                hotplug: false,
3557                acs_capabilities_supported: None,
3558            }
3559        );
3560
3561        assert_eq!(
3562            GenericPcieSwitchCli::from_str("rp2:sw,num_downstream_ports=8").unwrap(),
3563            GenericPcieSwitchCli {
3564                port_name: "rp2".to_string(),
3565                name: "sw".to_string(),
3566                num_downstream_ports: 8,
3567                hotplug: false,
3568                acs_capabilities_supported: None,
3569            }
3570        );
3571
3572        // Test hierarchical connections
3573        assert_eq!(
3574            GenericPcieSwitchCli::from_str("switch0-downstream-1:child_switch").unwrap(),
3575            GenericPcieSwitchCli {
3576                port_name: "switch0-downstream-1".to_string(),
3577                name: "child_switch".to_string(),
3578                num_downstream_ports: 4,
3579                hotplug: false,
3580                acs_capabilities_supported: None,
3581            }
3582        );
3583
3584        // Test hotplug flag
3585        assert_eq!(
3586            GenericPcieSwitchCli::from_str("rp0:switch0,hotplug").unwrap(),
3587            GenericPcieSwitchCli {
3588                port_name: "rp0".to_string(),
3589                name: "switch0".to_string(),
3590                num_downstream_ports: 4,
3591                hotplug: true,
3592                acs_capabilities_supported: None,
3593            }
3594        );
3595
3596        // Test hotplug with num_downstream_ports
3597        assert_eq!(
3598            GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=8,hotplug").unwrap(),
3599            GenericPcieSwitchCli {
3600                port_name: "rp0".to_string(),
3601                name: "switch0".to_string(),
3602                num_downstream_ports: 8,
3603                hotplug: true,
3604                acs_capabilities_supported: None,
3605            }
3606        );
3607
3608        assert_eq!(
3609            GenericPcieSwitchCli::from_str("rp0:switch0,acs=0").unwrap(),
3610            GenericPcieSwitchCli {
3611                port_name: "rp0".to_string(),
3612                name: "switch0".to_string(),
3613                num_downstream_ports: 4,
3614                hotplug: false,
3615                acs_capabilities_supported: Some(0),
3616            }
3617        );
3618
3619        assert_eq!(
3620            GenericPcieSwitchCli::from_str("rp0:switch0,acs=95").unwrap(),
3621            GenericPcieSwitchCli {
3622                port_name: "rp0".to_string(),
3623                name: "switch0".to_string(),
3624                num_downstream_ports: 4,
3625                hotplug: false,
3626                acs_capabilities_supported: Some(95),
3627            }
3628        );
3629
3630        // Error cases
3631        assert!(GenericPcieSwitchCli::from_str("").is_err());
3632        assert!(GenericPcieSwitchCli::from_str("switch0").is_err());
3633        assert!(GenericPcieSwitchCli::from_str("rp0:switch0:extra").is_err());
3634        assert!(GenericPcieSwitchCli::from_str("rp0:switch0,invalid_opt=value").is_err());
3635        assert!(GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=bad").is_err());
3636        assert!(GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=").is_err());
3637        assert!(GenericPcieSwitchCli::from_str("rp0:switch0,invalid_flag").is_err());
3638    }
3639
3640    #[test]
3641    fn test_pcie_remote_from_str() {
3642        // Basic port name only
3643        assert_eq!(
3644            PcieRemoteCli::from_str("rc0rp0").unwrap(),
3645            PcieRemoteCli {
3646                port_name: "rc0rp0".to_string(),
3647                socket_addr: None,
3648                hu: 0,
3649                controller: 0,
3650            }
3651        );
3652
3653        // With socket address
3654        assert_eq!(
3655            PcieRemoteCli::from_str("rc0rp0,socket=localhost:22567").unwrap(),
3656            PcieRemoteCli {
3657                port_name: "rc0rp0".to_string(),
3658                socket_addr: Some("localhost:22567".to_string()),
3659                hu: 0,
3660                controller: 0,
3661            }
3662        );
3663
3664        // With all options
3665        assert_eq!(
3666            PcieRemoteCli::from_str("myport,socket=localhost:22568,hu=1,controller=2").unwrap(),
3667            PcieRemoteCli {
3668                port_name: "myport".to_string(),
3669                socket_addr: Some("localhost:22568".to_string()),
3670                hu: 1,
3671                controller: 2,
3672            }
3673        );
3674
3675        // Only hu and controller
3676        assert_eq!(
3677            PcieRemoteCli::from_str("port0,hu=5,controller=3").unwrap(),
3678            PcieRemoteCli {
3679                port_name: "port0".to_string(),
3680                socket_addr: None,
3681                hu: 5,
3682                controller: 3,
3683            }
3684        );
3685
3686        // Error cases
3687        assert!(PcieRemoteCli::from_str("").is_err());
3688        assert!(PcieRemoteCli::from_str("port,socket=").is_err());
3689        assert!(PcieRemoteCli::from_str("port,hu=").is_err());
3690        assert!(PcieRemoteCli::from_str("port,hu=bad").is_err());
3691        assert!(PcieRemoteCli::from_str("port,controller=").is_err());
3692        assert!(PcieRemoteCli::from_str("port,controller=bad").is_err());
3693        assert!(PcieRemoteCli::from_str("port,unknown=value").is_err());
3694    }
3695
3696    #[test]
3697    fn test_parse_memory_units() {
3698        assert_eq!(parse_memory("64G").unwrap(), 64 * 1024 * 1024 * 1024);
3699        assert_eq!(parse_memory("64GB").unwrap(), 64 * 1024 * 1024 * 1024);
3700        assert_eq!(parse_memory("3MB").unwrap(), 3 * 1024 * 1024);
3701        assert_eq!(parse_memory("512KB").unwrap(), 512 * 1024);
3702        assert!(parse_memory("3MiB").is_err());
3703    }
3704
3705    #[test]
3706    fn test_memory_config_size_only() {
3707        assert_eq!(
3708            parse_memory_config("64G").unwrap(),
3709            MemoryCli {
3710                mem_size: 64 * 1024 * 1024 * 1024,
3711                shared: None,
3712                prefetch: false,
3713                transparent_hugepages: false,
3714                hugepages: false,
3715                hugepage_size: None,
3716                file: None,
3717            }
3718        );
3719    }
3720
3721    #[test]
3722    fn test_memory_config_key_value() {
3723        assert_eq!(
3724            parse_memory_config("size=2G,shared=off,prefetch=on,thp=on").unwrap(),
3725            MemoryCli {
3726                mem_size: 2 * 1024 * 1024 * 1024,
3727                shared: Some(false),
3728                prefetch: true,
3729                transparent_hugepages: true,
3730                hugepages: false,
3731                hugepage_size: None,
3732                file: None,
3733            }
3734        );
3735
3736        assert_eq!(
3737            parse_memory_config("size=4GB,hugepages=on,hugepage_size=2MB").unwrap(),
3738            MemoryCli {
3739                mem_size: 4 * 1024 * 1024 * 1024,
3740                shared: None,
3741                prefetch: false,
3742                transparent_hugepages: false,
3743                hugepages: true,
3744                hugepage_size: Some(2 * 1024 * 1024),
3745                file: None,
3746            }
3747        );
3748
3749        assert_eq!(
3750            parse_memory_config("file=/tmp/memory.bin").unwrap(),
3751            MemoryCli {
3752                mem_size: DEFAULT_MEMORY_SIZE,
3753                shared: None,
3754                prefetch: false,
3755                transparent_hugepages: false,
3756                hugepages: false,
3757                hugepage_size: None,
3758                file: Some(PathBuf::from("/tmp/memory.bin")),
3759            }
3760        );
3761    }
3762
3763    #[test]
3764    fn test_memory_config_rejects_invalid_combinations() {
3765        assert!(parse_memory_config("thp=on").is_err());
3766        assert!(parse_memory_config("size=1G,size=2G").is_err());
3767        assert!(parse_memory_config("hugepage_size=2M").is_err());
3768        assert!(parse_memory_config("hugepages=on,shared=off").is_err());
3769        assert!(parse_memory_config("hugepages=on,file=/tmp/memory.bin").is_err());
3770
3771        // Semantic validation of the hugepage size happens in the memory
3772        // builder, not in CLI parsing.
3773        assert_eq!(
3774            parse_memory_config("hugepages=on,hugepage_size=3MB")
3775                .unwrap()
3776                .hugepage_size,
3777            Some(3 * 1024 * 1024)
3778        );
3779    }
3780
3781    #[test]
3782    fn test_memory_options_merge_legacy_aliases() {
3783        let opt = Options::try_parse_from([
3784            "openvmm",
3785            "--memory",
3786            "2G",
3787            "--prefetch",
3788            "--private-memory",
3789            "--thp",
3790        ])
3791        .unwrap();
3792        opt.validate_memory_options().unwrap();
3793        assert_eq!(opt.memory_size(), 2 * 1024 * 1024 * 1024);
3794        assert!(opt.prefetch_memory());
3795        assert!(opt.private_memory());
3796        assert!(opt.transparent_hugepages());
3797    }
3798
3799    #[test]
3800    fn test_memory_options_allow_legacy_thp_with_new_private_memory() {
3801        let opt = Options::try_parse_from(["openvmm", "--memory", "shared=off", "--thp"]).unwrap();
3802        opt.validate_memory_options().unwrap();
3803        assert!(opt.private_memory());
3804        assert!(opt.transparent_hugepages());
3805    }
3806
3807    #[test]
3808    fn test_memory_options_reject_conflicting_legacy_aliases() {
3809        let opt = Options::try_parse_from(["openvmm", "--memory", "shared=on", "--private-memory"])
3810            .unwrap();
3811        assert!(opt.validate_memory_options().is_err());
3812    }
3813
3814    #[test]
3815    fn test_memory_options_reject_hugepage_legacy_conflicts() {
3816        let opt =
3817            Options::try_parse_from(["openvmm", "--memory", "hugepages=on", "--private-memory"])
3818                .unwrap();
3819        assert!(opt.validate_memory_options().is_err());
3820
3821        let opt = Options::try_parse_from([
3822            "openvmm",
3823            "--memory",
3824            "hugepages=on",
3825            "--memory-backing-file",
3826            "/tmp/memory.bin",
3827        ])
3828        .unwrap();
3829        assert!(opt.validate_memory_options().is_err());
3830    }
3831
3832    #[test]
3833    fn test_pidfile_option_parsed() {
3834        let opt = Options::try_parse_from(["openvmm", "--pidfile", "/tmp/test.pid"]).unwrap();
3835        assert_eq!(opt.pidfile, Some(PathBuf::from("/tmp/test.pid")));
3836    }
3837}