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 cxl_spec::CfmwsWindowRestrictions;
25use guid::Guid;
26use openvmm_defs::config::DEFAULT_PCAT_BOOT_ORDER;
27use openvmm_defs::config::DeviceVtl;
28use openvmm_defs::config::PcatBootDevice;
29use openvmm_defs::config::Vtl2BaseAddressType;
30use openvmm_defs::config::X2ApicConfig;
31use std::ffi::OsString;
32use std::net::SocketAddr;
33use std::path::PathBuf;
34use std::str::FromStr;
35use thiserror::Error;
36
37/// Parse CLI options, using a thread with a larger stack on Windows to avoid
38/// stack overflow in debug builds due to clap's deep stack usage.
39/// See <https://github.com/clap-rs/clap/issues/5134>.
40pub(crate) fn parse_options() -> Options {
41    // In non-optimized builds, clap uses an embarrassing amount of stack space
42    // to construct the `Command` instance for `Options`, more than the Windows
43    // default of 1MB. This has been known since 2023:
44    // <https://github.com/clap-rs/clap/issues/5134>, but no one has stepped up
45    // to fix it.
46    //
47    // Work around this by running the code on a thread with lots of stack
48    // space. This is easier and more reliable than configuring the PE binary to
49    // have a larger stack.
50    fn on_big_stack<R: Send>(f: impl Send + FnOnce() -> R) -> R {
51        if cfg!(windows) {
52            std::thread::scope(|s| {
53                std::thread::Builder::new()
54                    .stack_size(0x400000)
55                    .spawn_scoped(s, f)
56                    .unwrap()
57                    .join()
58                    .unwrap()
59            })
60        } else {
61            f()
62        }
63    }
64
65    on_big_stack(Options::parse)
66}
67
68const DEFAULT_MEMORY_SIZE: u64 = 1024 * 1024 * 1024;
69
70/// Guest memory configuration parsed from `--memory` (and, flattened, from
71/// `--numa`).
72///
73/// Fields are the raw parsed options; callers apply the defaults (`size`
74/// defaults to [`DEFAULT_MEMORY_SIZE`]; `transparent_hugepages` defaults to
75/// `!hugepages`). Cross-field validation lives in [`MemoryCli::validate`].
76#[derive(Debug, Clone, Default, PartialEq, Eq, vmm_cli::KeyValueArgs)]
77pub struct MemoryCli {
78    /// Guest RAM size. Defaults to [`DEFAULT_MEMORY_SIZE`] when unset.
79    pub size: Option<vmm_cli::MemorySize>,
80    /// Whether shared file-backed memory was explicitly requested (tri-state:
81    /// unset / `on` / `off`).
82    #[kv(flag)]
83    pub shared: Option<bool>,
84    /// Whether to prefetch guest RAM.
85    #[kv(flag)]
86    pub prefetch: bool,
87    /// Whether to use transparent huge pages. When unset, defaults to enabled
88    /// unless `hugepages` is set.
89    #[kv(flag, key = "thp")]
90    pub transparent_hugepages: Option<bool>,
91    /// Whether to use explicit hugetlb memfd backing for guest RAM.
92    #[kv(flag)]
93    pub hugepages: bool,
94    /// Explicit hugetlb page size.
95    pub hugepage_size: Option<vmm_cli::MemorySize>,
96    /// File used to back guest RAM.
97    pub file: Option<PathBuf>,
98}
99
100impl MemoryCli {
101    /// Validate cross-field constraints shared by `--memory` and `--numa`.
102    pub fn validate(&self) -> anyhow::Result<()> {
103        if self.hugepage_size.is_some() && !self.hugepages {
104            anyhow::bail!("hugepage_size requires hugepages=on");
105        }
106        if self.hugepages {
107            if self.shared == Some(false) {
108                anyhow::bail!("hugepages=on conflicts with shared=off");
109            }
110            if self.file.is_some() {
111                anyhow::bail!("hugepages=on conflicts with file=...");
112            }
113        }
114        Ok(())
115    }
116}
117
118/// NUMA node configuration parsed from `--numa`.
119#[derive(Debug, Clone, PartialEq, Eq, vmm_cli::KeyValueArgs)]
120pub struct NumaNodeCli {
121    /// Memory configuration (size, shared, prefetch, hugepages, etc.).
122    #[kv(flatten)]
123    pub memory: MemoryCli,
124    /// Host NUMA node to bind memory allocation to.
125    pub host_numa_node: Option<u32>,
126    /// Explicit VP indices for this node.
127    pub vps: Option<vmm_cli::BracketRangeList>,
128}
129
130/// NUMA distance parsed from `--numa-distance`.
131#[derive(Debug, Clone, PartialEq, Eq)]
132pub struct NumaDistanceCli {
133    /// Source node index.
134    pub src: u32,
135    /// Destination node index.
136    pub dst: u32,
137    /// Distance value (10-255, 255 = unreachable).
138    pub distance: u8,
139}
140
141/// OpenVMM virtual machine monitor.
142///
143/// This is not yet a stable interface and may change radically between
144/// versions.
145#[derive(Parser)]
146#[command(
147    name = "openvmm",
148    version = openvmm_build_info::get().version(),
149    long_version = openvmm_build_info::get().long_version(),
150)]
151pub struct Options {
152    /// processor count
153    #[clap(short = 'p', long, value_name = "COUNT", default_value = "1")]
154    pub processors: u32,
155
156    /// guest RAM configuration (`SIZE` or `key=value[,key=value...]`)
157    #[clap(
158        short = 'm',
159        long,
160        value_name = "PARAMS",
161        default_value = "1GB",
162        value_parser = parse_memory_config,
163        conflicts_with = "numa",
164        long_help = r#"Configure guest RAM.
165
166Syntax: SIZE | key=value[,key=value...]
167
168Size suffixes accept K, M, G, and T, optionally followed by B.
169
170Options:
171    size=<SIZE>              guest RAM size, default 1GB
172    shared[=on|off]          use shared file-backed RAM, default on
173    prefetch[=on|off]        pre-populate guest RAM mappings
174    thp[=on|off]             mark guest RAM as THP-eligible (Linux), default on
175    hugepages[=on|off]       allocate RAM from hugetlb/large pages (Linux, Windows)
176    hugepage_size=<SIZE>     hugepage size, default 2MB; requires hugepages=on
177    file=<PATH>              use an existing file as guest RAM backing
178
179Examples:
180    --memory 4G
181    --memory size=64GB,hugepages=on,hugepage_size=2MB
182    --memory size=4G,file=path/to/memory.bin
183    --memory size=4G,thp=off"#
184    )]
185    pub memory: MemoryCli,
186
187    /// NUMA node configuration (repeatable, one per node).
188    ///
189    /// Each --numa specifies one guest NUMA node. Mutually exclusive with
190    /// --memory.
191    #[clap(
192        long,
193        value_name = "PARAMS",
194        value_parser = parse_numa_node,
195        conflicts_with = "memory",
196        long_help = r#"Configure a guest NUMA node (repeatable, one per node).
197
198Syntax: key=value[,key=value...]
199
200Options:
201    size=<SIZE>              RAM for this node (required)
202    shared[=on|off]          use shared file-backed RAM, default on
203    prefetch[=on|off]        pre-populate guest RAM mappings
204    thp[=on|off]             mark node RAM as THP-eligible (Linux), default on
205    hugepages[=on|off]       allocate RAM from hugetlb/large pages (Linux, Windows)
206    hugepage_size=<SIZE>     hugepage size, default 2MB; requires hugepages=on
207    host_numa_node=<N>       bind allocation to host NUMA node N
208    vps=<LIST>               explicit VP indices (e.g. "[0,1,2,3]")
209
210  VP lists use bracket syntax with comma-separated indices and dash
211  ranges: vps=[0,1] or vps=[0-3] or vps=[0,1,4-5]. An empty list, vps=[],
212  declares a CPU-less node (e.g. a generic-initiator target); unlike a
213  non-empty list, it may be combined with nodes that omit vps.
214
215Examples:
216    --numa size=2G --numa size=2G
217    --numa size=2G,host_numa_node=0 --numa size=2G,host_numa_node=1
218    --numa size=2G,hugepages=on,vps=[0,1] --numa size=2G,vps=[2,3]
219    --numa size=2G,vps=[0-3] --numa size=2G,vps=[4-7]
220    --numa size=2G --numa size=0,vps=[]"#
221    )]
222    pub numa: Option<Vec<NumaNodeCli>>,
223
224    /// NUMA distance (repeatable). Format: SRC:DST:DISTANCE.
225    ///
226    /// SRC and DST are 0-based node indices. DISTANCE is 10-255 (10 = local, 255 = unreachable).
227    /// Specify each direction explicitly (not auto-symmetric).
228    #[clap(long, value_name = "SRC:DST:DIST", value_parser = parse_numa_distance, conflicts_with = "memory", requires = "numa")]
229    pub numa_distance: Option<Vec<NumaDistanceCli>>,
230
231    /// use shared memory segment
232    #[clap(short = 'M', long, hide = true)]
233    pub shared_memory: bool,
234
235    /// prefetch guest RAM
236    #[clap(long = "prefetch", hide = true, conflicts_with = "numa")]
237    pub deprecated_prefetch: bool,
238
239    /// back guest RAM with a file instead of anonymous memory.
240    /// The file is created/opened and sized to the guest RAM size.
241    /// Enables snapshot save (fsync) and restore (open + mmap).
242    #[clap(
243        long = "memory-backing-file",
244        value_name = "FILE",
245        hide = true,
246        conflicts_with_all = ["deprecated_private_memory", "numa"]
247    )]
248    pub deprecated_memory_backing_file: Option<PathBuf>,
249
250    /// Restore VM from a snapshot directory (implies file-backed memory from
251    /// the snapshot's memory.bin). Cannot be used with --memory-backing-file.
252    #[clap(
253        long,
254        value_name = "DIR",
255        conflicts_with_all = ["deprecated_memory_backing_file", "numa"]
256    )]
257    pub restore_snapshot: Option<PathBuf>,
258
259    /// use private anonymous memory for guest RAM
260    #[clap(long = "private-memory", hide = true, conflicts_with_all = ["deprecated_memory_backing_file", "restore_snapshot", "numa"])]
261    pub deprecated_private_memory: bool,
262
263    /// enable transparent huge pages for guest RAM (Linux only; deprecated, THP is on by default)
264    #[clap(long = "thp", hide = true, conflicts_with = "numa")]
265    pub deprecated_thp: bool,
266
267    /// start in paused state
268    #[clap(short = 'P', long)]
269    pub paused: bool,
270
271    /// kernel image (when using linux direct boot)
272    #[clap(short = 'k', long, value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_LINUX_DIRECT_KERNEL"))]
273    pub kernel: OptionalPathBuf,
274
275    /// initrd image (when using linux direct boot)
276    #[clap(short = 'r', long, value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_LINUX_DIRECT_INITRD"))]
277    pub initrd: OptionalPathBuf,
278
279    /// extra kernel command line args
280    #[clap(short = 'c', long, value_name = "STRING")]
281    pub cmdline: Vec<String>,
282
283    /// enable HV#1 capabilities
284    #[clap(long)]
285    pub hv: bool,
286
287    /// Boot UEFI without exposing hypervisor (HV#1) enlightenments. Requires
288    /// `--no-vmbus` since VMBus depends on the hypervisor.
289    #[clap(
290        long,
291        requires_all = ["uefi", "no_vmbus"],
292        conflicts_with_all = ["hv", "vtl2", "get", "pcat", "igvm"]
293    )]
294    pub no_hv: bool,
295
296    /// Use a full device tree instead of ACPI tables for ARM64 Linux direct
297    /// boot. By default, ARM64 uses ACPI mode (stub DT + EFI + ACPI tables).
298    /// This flag selects the legacy DT-only path. Rejected on x86.
299    #[clap(long, conflicts_with_all = ["uefi", "pcat", "igvm", "smbios"])]
300    pub device_tree: bool,
301
302    /// SMBIOS (DMI) identity overrides (repeatable).
303    #[clap(
304        long,
305        value_name = "PARAMS",
306        value_parser = parse_smbios,
307        long_help = r#"Override the guest's SMBIOS (DMI) identity.
308
309Syntax: type=N,key=value[,key=value...]
310
311`type` selects the SMBIOS structure and is required; there is no default.
312Unset keys use the loader's built-in default identity. Repeat --smbios to set
313fields across multiple structure types.
314
315OpenVMM Linux direct boot supports all listed fields. OpenHCL Linux direct
316boot and UEFI boot support Type 1 fields only. PCAT boot supports only Type 1
317`serial` and `uuid`. Unsupported fields are rejected with an error.
318
319Type 0 (BIOS Information):
320    vendor=<STRING>          BIOS vendor
321    version=<STRING>         BIOS version
322    date=<STRING>            BIOS release date
323    release=<MAJOR.MINOR>    System BIOS Major/Minor Release (e.g. 4.1)
324
325Type 1 (System Information):
326    manufacturer=<STRING>    system manufacturer (sys_vendor)
327    product=<STRING>         product name (product_name)
328    version=<STRING>         product version (product_version)
329    serial=<STRING>          serial number (product_serial)
330    uuid=<GUID|random>       system UUID (product_uuid); `random` generates a
331                             fresh per-VM GUID. Unset => all-zero GUID.
332    sku=<STRING>             SKU number (product_sku)
333    family=<STRING>          product family (product_family)
334
335Examples:
336    --smbios type=1,manufacturer=Contoso,product="Virtual Machine"
337    --smbios type=1,uuid=12345678-9abc-def0-1234-56789abcdef0
338    --smbios type=1,uuid=random
339    --smbios type=0,vendor=Contoso,version=1.0"#
340    )]
341    pub smbios: Vec<SmbiosCli>,
342
343    /// enable vtl2 - only supported in WHP and simulated without hypervisor support currently
344    ///
345    /// Currently implies --get.
346    #[clap(long, requires("hv"))]
347    pub vtl2: bool,
348
349    /// Add GET and related devices for using the OpenHCL paravisor to the
350    /// highest enabled VTL.
351    #[clap(long, requires("hv"))]
352    pub get: bool,
353
354    /// Disable GET and related devices for using the OpenHCL paravisor, even
355    /// when --vtl2 is passed.
356    #[clap(long, conflicts_with("get"))]
357    pub no_get: bool,
358
359    /// Run without VMBus, even if --hv or --uefi are specified.
360    #[clap(
361        long,
362        conflicts_with_all = [
363            "vmbus_vsock_path",
364            "vmbus_vtl2_vsock_path",
365            "vmbus_redirect",
366            "vmbus_max_version",
367            "vmbus_com1_serial",
368            "vmbus_com2_serial",
369            "vtl2",
370            "get",
371            "pcat",
372        ],
373    )]
374    pub no_vmbus: bool,
375
376    /// disable the VTL0 alias map presented to VTL2 by default
377    #[clap(long, requires("vtl2"))]
378    pub no_alias_map: bool,
379
380    /// enable isolation
381    #[clap(long)]
382    pub isolation: Option<IsolationCli>,
383
384    /// enable restricted interrupt injection for SNP Linux direct boot
385    #[clap(long)]
386    pub snp_restricted_injection: bool,
387
388    /// the hybrid vsock listener path
389    #[clap(long, value_name = "PATH", alias = "vsock-path")]
390    pub vmbus_vsock_path: Option<String>,
391
392    /// the VTL2 hybrid vsock listener path
393    #[clap(long, value_name = "PATH", requires("vtl2"), alias = "vtl2-vsock-path")]
394    pub vmbus_vtl2_vsock_path: Option<String>,
395
396    /// the late map vtl0 ram access policy when vtl2 is enabled
397    #[clap(long, requires("vtl2"), default_value = "halt")]
398    pub late_map_vtl0_policy: Vtl0LateMapPolicyCli,
399
400    /// attach a disk (can be passed multiple times)
401    #[clap(long_help = r#"
402e.g: --disk memdiff:file:/path/to/disk.vhd
403
404syntax: <path> | kind:<arg>[,flag,opt=arg,...]
405
406valid disk kinds:
407    `mem:<len>`                    memory backed disk
408        <len>: length of ramdisk, e.g.: `1G`
409    `memdiff:<disk>`               memory backed diff disk
410        <disk>: lower disk, e.g.: `file:base.img`
411    `file:<path>[;direct][;create=<len>]`   file-backed disk
412        <path>: path to file
413        `;direct`: bypass the OS page cache
414    `sql:<path>[;create=<len>]`    SQLite-backed disk (dev/test)
415    `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
416    `autocache:<key>:<disk>`       auto-cached SQLite layer (use `autocache::<disk>` to omit key; needs OPENVMM_AUTO_CACHE_PATH)
417    `blob:<type>:<url>`            HTTP blob (read-only)
418        <type>: `flat` or `vhd1`
419    `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
420        <cipher>: `xts-aes-256`
421    `prwrap:<disk>`                persistent reservations wrapper
422
423flags:
424    `ro`                           open disk as read-only
425    `dvd`                          specifies that device is cd/dvd and it is read_only
426    `vtl2`                         assign this disk to VTL2
427    `uh`                           relay this disk to VTL0 through SCSI-to-OpenHCL (show to VTL0 as SCSI)
428    `uh-nvme`                      relay this disk to VTL0 through NVMe-to-OpenHCL (show to VTL0 as SCSI)
429
430options:
431    `pcie_port=<name>`             present the disk using pcie under the specified port, incompatible with `dvd`, `vtl2`, `uh`, and `uh-nvme`
432    `on=<name>`                    attach to a named controller (NVMe or SCSI), incompatible with `pcie_port` and `vtl2`
433    `nsid=<N>`                     NVMe namespace ID (1-based), requires `on`; auto-assigned if omitted
434    `lun=<N>`                      SCSI LUN (0-based), requires `on`; auto-assigned if omitted
435    `relay=<ctrl>[:<loc>]`         relay through OpenHCL to the named OpenHCL controller, with optional location (LUN or NSID)
436"#)]
437    #[clap(long, value_name = "FILE")]
438    pub disk: Vec<DiskCli>,
439
440    /// \[deprecated\] attach a disk via an NVMe controller
441    ///
442    /// Use --nvme-pci and --disk on=\<name\> instead.
443    #[clap(long_help = r#"
444e.g: --nvme memdiff:file:/path/to/disk.vhd
445
446syntax: <path> | kind:<arg>[,flag,opt=arg,...]
447
448valid disk kinds:
449    `mem:<len>`                    memory backed disk
450        <len>: length of ramdisk, e.g.: `1G`
451    `memdiff:<disk>`               memory backed diff disk
452        <disk>: lower disk, e.g.: `file:base.img`
453    `file:<path>[;direct][;create=<len>]`   file-backed disk
454        <path>: path to file
455        `;direct`: bypass the OS page cache
456    `sql:<path>[;create=<len>]`    SQLite-backed disk (dev/test)
457    `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
458    `autocache:<key>:<disk>`       auto-cached SQLite layer (use `autocache::<disk>` to omit key; needs OPENVMM_AUTO_CACHE_PATH)
459    `blob:<type>:<url>`            HTTP blob (read-only)
460        <type>: `flat` or `vhd1`
461    `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
462        <cipher>: `xts-aes-256`
463    `prwrap:<disk>`                persistent reservations wrapper
464
465flags:
466    `ro`                           open disk as read-only
467    `vtl2`                         assign this disk to VTL2
468    `uh`                           relay this disk to VTL0 through SCSI-to-OpenHCL (show to VTL0 as NVMe)
469    `uh-nvme`                      relay this disk to VTL0 through NVMe-to-OpenHCL (show to VTL0 as NVMe)
470
471options:
472    `pcie_port=<name>`             present the disk using pcie under the specified port, incompatible with `vtl2`, `uh`, and `uh-nvme`
473"#)]
474    #[clap(long)]
475    pub nvme: Vec<DiskCli>,
476
477    /// create a named NVMe controller
478    #[clap(long_help = r#"
479Create a named NVMe controller with an explicit transport.
480
481syntax: id=<name>,pcie_port=<port> | id=<name>,vpci[=<guid>]
482
483The controller name can be referenced by `--disk` with the `on=<name>`
484option to attach namespaces to this controller.
485
486options:
487    `id=<name>`                    controller name (required)
488    `pcie_port=<port>`             present on PCIe under the specified port
489    `vpci[=<guid>]`                present via VPCI; optional instance GUID
490    `vtl2`                         assign to VTL2 (default VTL0)
491
492Exactly one of `pcie_port` or `vpci` must be specified.
493
494Examples:
495    --nvme-pci id=nvme0,pcie_port=p0
496    --nvme-pci id=nvme1,vpci
497    --nvme-pci id=nvme2,vpci=008091f6-9688-497d-9091-af347dc9173c
498"#)]
499    #[clap(long = "nvme-pci")]
500    pub nvme_pci: Vec<NvmeControllerCli>,
501
502    /// create a named VMBus SCSI controller
503    #[clap(long_help = r#"
504Create a named VMBus SCSI controller.
505
506syntax: id=<name>[,sub_channels=<N>][,vtl2]
507
508The controller name can be referenced by `--disk` with the `on=<name>`
509option to attach disks to this controller.
510
511options:
512    `id=<name>`                    controller name (required)
513    `sub_channels=<N>`             number of sub-channels (default 0)
514    `vtl2`                         assign to VTL2 (default VTL0)
515
516Examples:
517    --vmbus-scsi id=scsi0
518    --vmbus-scsi id=scsi1,sub_channels=4
519"#)]
520    #[clap(long = "vmbus-scsi")]
521    pub vmbus_scsi: Vec<ScsiControllerCli>,
522
523    /// register an OpenHCL-managed storage controller (relay target)
524    #[clap(long_help = r#"
525Register an OpenHCL-managed storage controller that can be used as a
526relay target with `--disk ... relay=<name>`.
527
528syntax: id=<name>,type=scsi|nvme[,guid=<guid>]
529
530options:
531    `id=<name>`                    controller name (required)
532    `type=scsi|nvme`               controller protocol (required)
533    `guid=<guid>`                  instance GUID (auto-derived from name if omitted)
534
535Examples:
536    --openhcl-controller id=vtl0-scsi,type=scsi
537    --openhcl-controller id=vtl0-nvme,type=nvme,guid=09a59b81-...
538"#)]
539    #[clap(long = "openhcl-controller")]
540    pub openhcl_controller: Vec<OpenhclControllerCli>,
541
542    /// attach a CXL Type-3 test endpoint on a PCIe root port
543    #[clap(long = "cxl-test", value_name = "mem:<len>,pcie_port=<name>")]
544    pub cxl_test: Vec<CxlTestDeviceCli>,
545
546    /// attach a disk via a virtio-blk controller
547    #[clap(long_help = r#"
548e.g: --virtio-blk memdiff:file:/path/to/disk.vhd
549
550syntax: <path> | kind:<arg>[,flag,opt=arg,...]
551
552valid disk kinds:
553    `mem:<len>`                    memory backed disk
554        <len>: length of ramdisk, e.g.: `1G`
555    `memdiff:<disk>`               memory backed diff disk
556        <disk>: lower disk, e.g.: `file:base.img`
557    `file:<path>[;direct]`                  file-backed disk
558        <path>: path to file
559        `;direct`: bypass the OS page cache
560
561flags:
562    `ro`                           open disk as read-only
563
564options:
565    `pcie_port=<name>`             present the disk using pcie under the specified port
566    `serial=<value>`               1-20 printable ASCII bytes except comma and brackets
567"#)]
568    #[clap(long = "virtio-blk")]
569    pub virtio_blk: Vec<DiskCli>,
570
571    /// Attach a vhost-user device via a Unix socket.
572    ///
573    /// The first positional argument is the socket path. Options:
574    ///
575    /// ```text
576    ///   type=blk|fs                        — device type (shorthand)
577    ///   device_id=N                        — numeric virtio device ID
578    ///   tag=NAME                           — mount tag (required for type=fs)
579    ///   num_queues=N                       — queue count (type=blk/fs only)
580    ///   queue_size=N                       — per-queue size (type=blk/fs only)
581    ///   queue_sizes=[N,N,N]                — per-queue sizes (device_id= only)
582    ///   pcie_port=NAME                     — present on PCIe under the specified port
583    /// ```
584    ///
585    /// Examples:
586    ///
587    /// ```text
588    ///   --vhost-user /tmp/vhost.sock,type=blk
589    ///   --vhost-user /tmp/vhost.sock,type=blk,num_queues=4,queue_size=512
590    ///   --vhost-user /tmp/vhost.sock,device_id=2,queue_sizes=[128,128]
591    ///   --vhost-user /tmp/vhost.sock,type=blk,pcie_port=port0
592    ///   --vhost-user /tmp/virtiofsd.sock,type=fs,tag=myfs
593    ///   --vhost-user /tmp/virtiofsd.sock,type=fs,tag=myfs,num_queues=2,queue_size=1024
594    /// ```
595    #[cfg(target_os = "linux")]
596    #[clap(long = "vhost-user")]
597    pub vhost_user: Vec<VhostUserCli>,
598
599    /// number of sub-channels for the SCSI controller
600    #[clap(long, value_name = "COUNT", default_value = "0")]
601    pub scsi_sub_channels: u16,
602
603    /// expose a virtual NIC
604    #[clap(long)]
605    pub nic: bool,
606
607    /// expose a virtual NIC with the given backend (consomme | dio | tap | none)
608    ///
609    /// Prefix with `uh:` to add this NIC via Mana emulation through OpenHCL,
610    /// `vtl2:` to assign this NIC to VTL2, or `pcie_port=<port_name>:` to
611    /// expose the NIC over emulated PCIe at the specified port.
612    ///
613    /// For consomme, forward host ports into the guest with `hostfwd=`:
614    ///   --net consomme:hostfwd=tcp::3389-:3389
615    ///   --net consomme:hostfwd=tcp:127.0.0.1:8080-:80
616    ///   --net consomme:hostfwd=tcp:\[::1\]:8080-:80
617    ///   --net consomme:10.0.0.0/24,hostfwd=tcp::22-:22,hostfwd=udp::5000-:5000
618    #[clap(long)]
619    pub net: Vec<NicConfigCli>,
620
621    /// expose a virtual NIC using the Windows kernel-mode vmswitch.
622    ///
623    /// Specify the switch ID or "default" for the default switch.
624    #[clap(long, value_name = "SWITCH_ID")]
625    pub kernel_vmnic: Vec<String>,
626
627    /// expose a graphics device
628    #[clap(long)]
629    pub gfx: bool,
630
631    /// support a graphics device in vtl2
632    #[clap(long, requires("vtl2"), conflicts_with("gfx"))]
633    pub vtl2_gfx: bool,
634
635    /// VNC server configuration (listen address, port, client limit, etc.).
636    #[clap(flatten)]
637    pub vnc: VncCli,
638
639    /// set the APIC ID offset, for testing APIC IDs that don't match VP index
640    #[cfg(guest_arch = "x86_64")]
641    #[clap(long, default_value_t)]
642    pub apic_id_offset: u32,
643
644    /// the maximum number of VPs per socket
645    #[clap(long)]
646    pub vps_per_socket: Option<u32>,
647
648    /// enable or disable SMT (hyperthreading) (auto | force | off)
649    #[clap(long, default_value = "auto")]
650    pub smt: SmtConfigCli,
651
652    /// configure x2apic (auto | supported | off | on)
653    #[cfg(guest_arch = "x86_64")]
654    #[clap(long, default_value = "auto", value_parser = parse_x2apic)]
655    pub x2apic: X2ApicConfig,
656
657    /// configure PCIe MSI controller for aarch64 (auto | its | v2m)
658    #[cfg(guest_arch = "aarch64")]
659    #[clap(long, default_value = "auto")]
660    pub gic_msi: GicMsiCli,
661
662    /// configure SMMUv3 IOMMU for an aarch64 PCIe root complex (repeatable).
663    ///
664    /// Syntax: `rc=<name>[,accel][,oas=auto|N]`.
665    #[cfg(guest_arch = "aarch64")]
666    #[clap(long, value_name = "SMMU_CONFIG")]
667    pub smmu: Vec<SmmuCli>,
668
669    /// COM1 binding, optionally prefixed with `debugger-mode:` (see below)
670    /// (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>]\[,name=\<windowtitle\>\] | null | none)
671    ///
672    /// Prefix the binding with `debugger-mode:` to run this COM port in
673    /// debugger mode for WinDbg kernel debugging over serial (KD), e.g.
674    /// `--com1 debugger-mode:listen=<path>` or
675    /// `--com1 debugger-mode:listen=tcp:<ip>:<port>`. In debugger mode OpenVMM
676    /// keeps this port's backend drained and may drop bytes instead of applying
677    /// backpressure, so the KD transport does not deadlock across guest
678    /// resets/reboots (KD recovers dropped bytes via its own retransmission).
679    /// Debugger mode is independent per COM port.
680    #[clap(long, value_name = "SERIAL")]
681    pub com1: Option<ComSerialConfigCli>,
682
683    /// COM2 binding, optionally prefixed with `debugger-mode:` (see --com1)
684    /// (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>]\[,name=\<windowtitle\>\] | null | none)
685    #[clap(long, value_name = "SERIAL")]
686    pub com2: Option<ComSerialConfigCli>,
687
688    /// COM3 binding, optionally prefixed with `debugger-mode:` (see --com1)
689    /// (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>]\[,name=\<windowtitle\>\] | null | none)
690    #[clap(long, value_name = "SERIAL")]
691    pub com3: Option<ComSerialConfigCli>,
692
693    /// COM4 binding, optionally prefixed with `debugger-mode:` (see --com1)
694    /// (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>]\[,name=\<windowtitle\>\] | null | none)
695    #[clap(long, value_name = "SERIAL")]
696    pub com4: Option<ComSerialConfigCli>,
697
698    /// vmbus com1 serial binding (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>]\[,name=\<windowtitle\>\] | null | none)
699    #[structopt(long, value_name = "SERIAL")]
700    pub vmbus_com1_serial: Option<SerialConfigCli>,
701
702    /// vmbus com2 serial binding (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>]\[,name=\<windowtitle\>\] | null | none)
703    #[structopt(long, value_name = "SERIAL")]
704    pub vmbus_com2_serial: Option<SerialConfigCli>,
705
706    /// Only allow guest to host serial traffic
707    #[clap(long)]
708    pub serial_tx_only: bool,
709
710    /// 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\>\] | null | none))
711    #[clap(long, value_name = "SERIAL")]
712    pub debugcon: Option<DebugconSerialConfigCli>,
713
714    /// boot UEFI firmware, optionally configured with comma-separated options
715    #[clap(
716        long,
717        short = 'e',
718        value_name = "OPTIONS",
719        num_args = 0..=1,
720        default_missing_value = ""
721    )]
722    pub uefi: Option<UefiCli>,
723
724    /// UEFI firmware file (deprecated; use --uefi firmware=FILE)
725    #[clap(
726        long = "uefi-firmware",
727        hide = true,
728        requires("uefi"),
729        conflicts_with("igvm"),
730        value_name = "FILE"
731    )]
732    pub deprecated_uefi_firmware: Option<PathBuf>,
733
734    /// enable UEFI debugging on COM1 (deprecated; use --uefi debug)
735    #[clap(long = "uefi-debug", hide = true, requires("uefi"))]
736    pub deprecated_uefi_debug: bool,
737
738    /// enable memory protections in UEFI (deprecated; use --uefi enable_memory_protections)
739    #[clap(long = "uefi-enable-memory-protections", hide = true, requires("uefi"))]
740    pub deprecated_uefi_enable_memory_protections: bool,
741
742    /// force UEFI to bounce-buffer all DMA traffic (deprecated; use --uefi force_dma_bounce)
743    #[clap(long = "uefi-force-dma-bounce", hide = true, requires("uefi"))]
744    pub deprecated_uefi_force_dma_bounce: bool,
745
746    /// continue with invalid UEFI firmware version information (deprecated; use --uefi force_firmware_version)
747    #[clap(long = "uefi-force-firmware-version", hide = true, requires("uefi"))]
748    pub deprecated_uefi_force_firmware_version: bool,
749
750    /// set PCAT boot order as comma-separated string of boot device types
751    /// (e.g: floppy,hdd,optical,net).
752    ///
753    /// If less than 4 entries are added, entries are added according to their
754    /// default boot order (optical,hdd,net,floppy)
755    ///
756    /// e.g: passing "floppy,optical" will result in a boot order equivalent to
757    /// "floppy,optical,hdd,net".
758    ///
759    /// Passing duplicate types is an error.
760    #[clap(long, requires("pcat"))]
761    pub pcat_boot_order: Option<PcatBootOrderCli>,
762
763    /// Boot with PCAT BIOS firmware and piix4 devices
764    #[clap(long, conflicts_with("uefi"))]
765    pub pcat: bool,
766
767    /// PCAT firmware file
768    #[clap(long, requires("pcat"), value_name = "FILE")]
769    pub pcat_firmware: Option<PathBuf>,
770
771    /// boot an IGVM with required firmware and personality options
772    #[clap(
773        long,
774        conflicts_with_all = ["kernel", "pcat"],
775        value_name = "OPTIONS",
776        long_help = r#"Boot an IGVM file.
777
778Syntax: --igvm firmware=<FILE>,personality=<openhcl|uefi|linux-direct>
779
780Both firmware and personality are required; neither has a default.
781The openhcl personality requires explicit --hv --vtl2."#
782    )]
783    pub igvm: Option<IgvmCli>,
784
785    /// specify igvm vtl2 relocation type
786    /// (absolute=\<addr\>, disable, auto=\<filesize,or memory size\>, vtl2=\<filesize,or memory size\>,)
787    #[clap(long, requires("igvm"), default_value = "auto=filesize", value_parser = parse_vtl2_relocation)]
788    pub igvm_vtl2_relocation_type: Vtl2BaseAddressType,
789
790    /// add a virtio_9p device (e.g. myfs,C:\)
791    ///
792    /// Prefix with `pcie_port=<port_name>:` to expose the device over
793    /// emulated PCIe at the specified port.
794    #[clap(long, value_name = "[pcie_port=PORT:]tag,root_path")]
795    pub virtio_9p: Vec<FsArgs>,
796
797    /// output debug info from the 9p server
798    #[clap(long)]
799    pub virtio_9p_debug: bool,
800
801    /// add a virtio_fs device (e.g. myfs,C:\,uid=1000,gid=2000)
802    ///
803    /// Prefix with `pcie_port=<port_name>:` to expose the device over
804    /// emulated PCIe at the specified port.
805    #[clap(long, value_name = "[pcie_port=PORT:]tag,root_path,[options]")]
806    pub virtio_fs: Vec<FsArgsWithOptions>,
807
808    /// add a virtio_fs device for sharing memory (e.g. myfs,\SectionDirectoryPath)
809    ///
810    /// Prefix with `pcie_port=<port_name>:` to expose the device over
811    /// emulated PCIe at the specified port.
812    #[clap(long, value_name = "[pcie_port=PORT:]tag,root_path")]
813    pub virtio_fs_shmem: Vec<FsArgs>,
814
815    /// add a virtio_fs device under the selected bus (auto | mmio | pci | pcie:port | vpci)
816    #[clap(long, value_name = "BUS", default_value = "auto")]
817    pub virtio_fs_bus: VirtioBusCli,
818
819    /// virtio PMEM device
820    ///
821    /// Prefix with `pcie_port=<port_name>:` to expose the device over
822    /// emulated PCIe at the specified port.
823    #[clap(long, value_name = "[pcie_port=PORT:]PATH")]
824    pub virtio_pmem: Option<VirtioPmemArgs>,
825
826    /// add a virtio entropy (RNG) device
827    #[clap(long)]
828    pub virtio_rng: bool,
829
830    /// add a virtio-rng device under the selected bus (auto | mmio | pci | pcie:port | vpci)
831    #[clap(long, value_name = "BUS", default_value = "auto")]
832    pub virtio_rng_bus: VirtioBusCli,
833
834    /// attach the virtio-rng device to the specified PCIe port (overrides --virtio-rng-bus)
835    #[clap(long, value_name = "PORT", requires("virtio_rng"))]
836    pub virtio_rng_pcie_port: Option<String>,
837
838    /// add a virtio real-time clock device, optionally configured with
839    /// comma-separated options (bus=auto|mmio|pci|pcie:PORT|vpci)
840    #[clap(
841        long,
842        value_name = "OPTIONS",
843        num_args = 0..=1,
844        default_missing_value = ""
845    )]
846    pub virtio_rtc: Option<VirtioRtcCli>,
847
848    /// virtio console device backed by a serial backend (/dev/hvc0 in guest)
849    ///
850    /// Accepts serial config (console | stderr | listen=\<path\> |
851    /// file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> |
852    /// term[=\<program\>]\[,name=\<windowtitle\>\] | null | none)
853    #[clap(long)]
854    pub virtio_console: Option<SerialConfigCli>,
855
856    /// attach the virtio-console device to the specified PCIe port
857    #[clap(long, value_name = "PORT", requires("virtio_console"))]
858    pub virtio_console_pcie_port: Option<String>,
859
860    /// select the bus for virtio vsock devices (pci | pcie\[:port\] | mmio)
861    #[clap(long, value_name = "BUS", value_parser = parse_virtio_vsock_bus)]
862    pub virtio_vsock_bus: Option<VirtioBusCli>,
863
864    /// add a virtio vsock device with the given Unix socket base path
865    #[clap(long, value_name = "PATH")]
866    pub virtio_vsock_path: Option<String>,
867
868    /// expose the guest in the host AF_VSOCK namespace using the Linux
869    /// vhost_vsock kernel backend
870    #[cfg(target_os = "linux")]
871    #[clap(
872        long,
873        value_name = "CID",
874        conflicts_with = "virtio_vsock_path",
875        value_parser = parse_vhost_vsock_cid
876    )]
877    pub virtio_vsock_vhost_cid: Option<u32>,
878
879    /// expose a virtio network with the given backend (dio | vmnic | tap |
880    /// none)
881    ///
882    /// Prefix with `uh:` to add this NIC via Mana emulation through OpenHCL,
883    /// `vtl2:` to assign this NIC to VTL2, or `pcie_port=<port_name>:` to
884    /// expose the NIC over emulated PCIe at the specified port.
885    #[clap(long)]
886    pub virtio_net: Vec<NicConfigCli>,
887
888    /// send log output from the worker process to a file instead of stderr. the file will be overwritten.
889    #[clap(long, value_name = "PATH")]
890    pub log_file: Option<PathBuf>,
891
892    /// write the process ID to the specified file on startup, and remove it on
893    /// exit. the file is not removed if the process is killed with SIGKILL or
894    /// crashes. no file locking is performed.
895    #[clap(long, value_name = "PATH")]
896    pub pidfile: Option<PathBuf>,
897
898    /// \[deprecated\] run as a ttrpc server on the specified Unix socket
899    ///
900    /// Use `--rpc path=<PATH>,transport=ttrpc` instead.
901    #[clap(long, value_name = "SOCKETPATH")]
902    pub ttrpc: Option<PathBuf>,
903
904    /// \[deprecated\] run as a grpc server on the specified Unix socket
905    ///
906    /// Use `--rpc path=<PATH>,transport=grpc` instead.
907    #[clap(long, value_name = "SOCKETPATH", conflicts_with("ttrpc"))]
908    pub grpc: Option<PathBuf>,
909
910    /// run as an RPC server on the specified Unix socket
911    #[clap(long_help = r#"
912Run as an RPC server on the specified Unix socket.
913
914syntax: path=<PATH>[,transport=<TRANSPORT>]
915
916options:
917    `path=<PATH>`                  Unix socket path to listen on (required)
918    `transport=<TRANSPORT>`        wire transport to accept (default: auto)
919
920valid transports:
921    `auto`                         auto-detect ttrpc vs. gRPC per connection
922    `ttrpc`                        accept ttrpc clients only
923    `grpc`                         accept gRPC clients only
924
925Examples:
926    --rpc path=/tmp/openvmm.sock
927    --rpc path=/tmp/openvmm.sock,transport=ttrpc
928"#)]
929    #[clap(
930        long,
931        value_name = "path=PATH[,transport=TRANSPORT]",
932        conflicts_with("ttrpc"),
933        conflicts_with("grpc")
934    )]
935    pub rpc: Option<RpcCli>,
936
937    /// do not launch child processes
938    #[clap(long)]
939    pub single_process: bool,
940
941    /// device to assign (can be passed multiple times)
942    #[cfg(windows)]
943    #[clap(long, value_name = "PATH")]
944    pub device: Vec<String>,
945
946    /// instead of showing the frontpage the VM will shutdown instead (deprecated; use --uefi disable_frontpage)
947    #[clap(long = "disable-frontpage", hide = true, requires("uefi"))]
948    pub deprecated_disable_frontpage: bool,
949
950    /// add a vtpm device, optionally selecting version 138 or 185 (default: 185)
951    #[clap(
952        long,
953        value_name = "VERSION",
954        num_args = 0..=1,
955        default_missing_value = "185"
956    )]
957    pub tpm: Option<TpmVersionCli>,
958
959    /// the mesh worker host name.
960    ///
961    /// Used internally for debugging and diagnostics.
962    #[clap(long, default_value = "control", hide(true))]
963    #[expect(clippy::option_option)]
964    pub internal_worker: Option<Option<String>>,
965
966    /// redirect the VTL 0 vmbus control plane to a proxy in VTL 2.
967    #[clap(long, requires("vtl2"))]
968    pub vmbus_redirect: bool,
969
970    /// limit the maximum protocol version allowed by vmbus; used for testing purposes
971    #[clap(long, value_parser = vmbus_core::parse_vmbus_version)]
972    pub vmbus_max_version: Option<u32>,
973
974    /// The disk to use for the VMGS.
975    ///
976    /// If this is not provided, guest state will be stored in memory.
977    #[clap(long_help = r#"
978e.g: --vmgs memdiff:file:/path/to/file.vmgs
979
980syntax: <path> | kind:<arg>[,flag]
981
982valid disk kinds:
983    `mem:<len>`                     memory backed disk
984        <len>: length of ramdisk, e.g.: `1G` or `VMGS_DEFAULT`
985    `memdiff:<disk>[;create=<len>]` memory backed diff disk
986        <disk>: lower disk, e.g.: `file:base.img`
987    `file:<path>`                   file-backed disk
988        <path>: path to file
989
990flags:
991    `fmt`                           reprovision the VMGS before boot
992    `fmt-on-fail`                   reprovision the VMGS before boot if it is corrupted
993"#)]
994    #[clap(long)]
995    pub vmgs: Option<VmgsCli>,
996
997    /// Use GspById guest state encryption policy with a test seed
998    #[clap(long, requires("vmgs"))]
999    pub test_gsp_by_id: bool,
1000
1001    /// VGA firmware file
1002    #[clap(long, requires("pcat"), value_name = "FILE")]
1003    pub vga_firmware: Option<PathBuf>,
1004
1005    /// enable secure boot
1006    #[clap(long)]
1007    pub secure_boot: bool,
1008
1009    /// use secure boot template
1010    #[clap(long)]
1011    pub secure_boot_template: Option<SecureBootTemplateCli>,
1012
1013    /// custom uefi nvram json file
1014    #[clap(long, value_name = "PATH")]
1015    pub custom_uefi_json: Option<PathBuf>,
1016
1017    /// the path to a named pipe (Windows) or Unix socket (Linux) to relay to the connected
1018    /// tty.
1019    ///
1020    /// This is a hidden argument used internally.
1021    #[clap(long, hide(true))]
1022    pub relay_console_path: Option<PathBuf>,
1023
1024    /// the title of the console window spawned from the relay console.
1025    ///
1026    /// This is a hidden argument used internally.
1027    #[clap(long, hide(true))]
1028    pub relay_console_title: Option<String>,
1029
1030    /// enable in-hypervisor gdb debugger
1031    #[clap(long, value_name = "PORT")]
1032    pub gdb: Option<u16>,
1033
1034    /// enable emulated MANA devices with the given network backend (see --net)
1035    ///
1036    /// Prefix with `pcie_port=<port_name>:` to expose the nic over emulated PCIe
1037    /// at the specified port.
1038    #[clap(long)]
1039    pub mana: Vec<NicConfigCli>,
1040
1041    /// use a specific hypervisor interface, with optional backend-specific
1042    /// parameters.
1043    ///
1044    /// Format: `name` or `name:key=val,key,...`
1045    ///
1046    /// WHP parameters (x86_64 guests only):
1047    ///   user_mode_apic       - use user-mode APIC emulator
1048    ///   no_enlightenments    - disable in-hypervisor enlightenments
1049    ///
1050    /// Examples:
1051    ///   --hypervisor whp
1052    ///   --hypervisor whp:user_mode_apic
1053    ///   --hypervisor whp:user_mode_apic,no_enlightenments
1054    ///   --hypervisor kvm
1055    #[clap(long)]
1056    pub hypervisor: Option<String>,
1057
1058    /// expose hardware virtualization (VMX/SVM) to the guest so it can run its
1059    /// own hypervisor.
1060    ///
1061    /// Only supported on x86_64, and only by backends that support nested
1062    /// virtualization (currently WHP and KVM). Requires host support.
1063    #[clap(long)]
1064    pub nested_virt: bool,
1065
1066    /// attach an ide drive (can be passed multiple times)
1067    ///
1068    /// Each ide controller has two channels. Each channel can have up to two
1069    /// attachments.
1070    ///
1071    /// If the `s` flag is not passed then the drive will we be attached to the
1072    /// primary ide channel if space is available. If two attachments have already
1073    /// been added to the primary channel then the drive will be attached to the
1074    /// secondary channel.
1075    #[clap(long_help = r#"
1076e.g: --ide memdiff:file:/path/to/disk.vhd
1077
1078syntax: <path> | kind:<arg>[,flag,opt=arg,...]
1079
1080valid disk kinds:
1081    `mem:<len>`                    memory backed disk
1082        <len>: length of ramdisk, e.g.: `1G`
1083    `memdiff:<disk>`               memory backed diff disk
1084        <disk>: lower disk, e.g.: `file:base.img`
1085    `file:<path>[;create=<len>]`   file-backed disk
1086        <path>: path to file
1087    `sql:<path>[;create=<len>]`    SQLite-backed disk (dev/test)
1088    `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
1089    `blob:<type>:<url>`            HTTP blob (read-only)
1090        <type>: `flat` or `vhd1`
1091    `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
1092        <cipher>: `xts-aes-256`
1093
1094additional wrapper kinds (e.g., `autocache`, `prwrap`) are also supported;
1095this list is not exhaustive.
1096
1097flags:
1098    `ro`                           open disk as read-only
1099    `s`                            attach drive to secondary ide channel
1100    `dvd`                          specifies that device is cd/dvd and it is read_only
1101"#)]
1102    #[clap(long, value_name = "FILE", requires("pcat"))]
1103    pub ide: Vec<IdeDiskCli>,
1104
1105    /// attach a floppy drive (should be able to be passed multiple times). VM must be generation 1 (no UEFI)
1106    ///
1107    #[clap(long_help = r#"
1108e.g: --floppy memdiff:file:/path/to/disk.vfd,ro
1109
1110syntax: <path> | kind:<arg>[,flag,opt=arg,...]
1111
1112valid disk kinds:
1113    `mem:<len>`                    memory backed disk
1114        <len>: length of ramdisk, e.g.: `1G`
1115    `memdiff:<disk>`               memory backed diff disk
1116        <disk>: lower disk, e.g.: `file:base.img`
1117    `file:<path>[;create=<len>]`   file-backed disk
1118        <path>: path to file
1119    `sql:<path>[;create=<len>]`    SQLite-backed disk (dev/test)
1120    `sqldiff:<path>[;create]:<disk>` SQLite diff layer on a backing disk
1121    `blob:<type>:<url>`            HTTP blob (read-only)
1122        <type>: `flat` or `vhd1`
1123    `crypt:<cipher>:<key_file>:<disk>` encrypted disk wrapper
1124        <cipher>: `xts-aes-256`
1125
1126flags:
1127    `ro`                           open disk as read-only
1128"#)]
1129    #[clap(long, value_name = "FILE", requires("pcat"))]
1130    pub floppy: Vec<FloppyDiskCli>,
1131
1132    /// enable guest watchdog device
1133    #[clap(long)]
1134    pub guest_watchdog: bool,
1135
1136    /// Enable OpenHCL's crash dump device, writing ELF core dumps of
1137    /// VTL2 user-mode components of OpenHCL in the given directory.
1138    #[clap(long)]
1139    pub openhcl_dump_path: Option<PathBuf>,
1140
1141    /// what to do when the guest requests a reset: reset it (default), halt the
1142    /// VM for inspection, or exit the VMM process (use `exit:<code>` to set the
1143    /// exit status)
1144    #[clap(long, value_name = "ACTION", default_value = "reset", value_parser = parse_guest_power_action)]
1145    pub guest_reset_action: GuestPowerAction,
1146
1147    /// what to do when the guest powers off or hibernates: halt the VM for
1148    /// inspection (default), reset it, or exit the VMM process (use
1149    /// `exit:<code>` to set the exit status)
1150    #[clap(long, value_name = "ACTION", default_value = "halt", value_parser = parse_guest_power_action)]
1151    pub guest_shutdown_action: GuestPowerAction,
1152
1153    /// what to do when the guest triple-faults: halt the VM for inspection
1154    /// (default), reset it, or exit the VMM process (use `exit:<code>` to set
1155    /// the exit status)
1156    #[clap(long, value_name = "ACTION", default_value = "halt", value_parser = parse_guest_power_action)]
1157    pub guest_crash_action: GuestPowerAction,
1158
1159    /// when the guest triple-faults, write a WinDbg-compatible `.vmrs` dump of
1160    /// the whole VM's VP state and guest memory to the specified path before
1161    /// applying the crash action
1162    ///
1163    /// This is a host-side, whole-VM dump triggered by a triple fault, distinct
1164    /// from `--openhcl-dump-path` (which captures an ELF core dump of user-mode
1165    /// components in OpenHCL).
1166    #[clap(long, value_name = "PATH")]
1167    pub crash_dump_path: Option<PathBuf>,
1168
1169    /// what to do when the guest watchdog fires (the guest stopped petting it):
1170    /// reset the VM (default), halt it for inspection, or exit the VMM process
1171    /// (use `exit:<code>` to set the exit status). Requires `--guest-watchdog`.
1172    #[clap(long, value_name = "ACTION", default_value = "reset", value_parser = parse_guest_power_action, requires = "guest_watchdog")]
1173    pub guest_watchdog_action: GuestPowerAction,
1174
1175    /// write saved state .proto files to the specified path
1176    #[clap(long)]
1177    pub write_saved_state_proto: Option<PathBuf>,
1178
1179    /// specify the IMC hive file for booting Windows
1180    #[clap(long)]
1181    pub imc: Option<PathBuf>,
1182
1183    /// expose a battery device
1184    #[clap(long)]
1185    pub battery: bool,
1186
1187    /// enable guest hibernation
1188    #[clap(long)]
1189    pub hibernation: bool,
1190
1191    /// set the UEFI console mode (deprecated; use --uefi console=MODE)
1192    #[clap(long = "uefi-console-mode", hide = true, requires("uefi"))]
1193    pub deprecated_uefi_console_mode: Option<UefiConsoleModeCli>,
1194
1195    /// set the EFI diagnostics log level
1196    #[clap(long_help = r#"
1197Set the EFI diagnostics log level.
1198
1199options:
1200    default                        default (ERROR and WARN only)
1201    info                           info (ERROR, WARN, and INFO)
1202    full                           full (all log levels)
1203"#)]
1204    #[clap(long = "efi-diagnostics-log-level", hide = true, requires("uefi"))]
1205    pub deprecated_efi_diagnostics_log_level: Option<EfiDiagnosticsLogLevelCli>,
1206
1207    /// Perform a default boot even if boot entries exist and fail (deprecated; use --uefi default_boot_always_attempt)
1208    #[clap(long = "default-boot-always-attempt", hide = true)]
1209    pub deprecated_default_boot_always_attempt: bool,
1210
1211    /// Enable AMD IOMMU (AMD-Vi) emulation on specified root complexes.
1212    /// Repeat for each root complex that should have an IOMMU, e.g.:
1213    ///   --amd-iommu rc0 --amd-iommu rc1
1214    /// The IOMMU appears at device 0 function 0 on each specified root
1215    /// complex. Requires --pcie-root-complex.
1216    #[cfg(guest_arch = "x86_64")]
1217    #[clap(long)]
1218    pub amd_iommu: Vec<String>,
1219
1220    /// Enable Intel VT-d IOMMU emulation on specified root complexes.
1221    /// Repeat for each root complex that should have an IOMMU, e.g.:
1222    ///   --intel-vtd rc0 --intel-vtd rc1
1223    /// Mutually exclusive with --amd-iommu within the same VM.
1224    /// Requires --pcie-root-complex.
1225    #[cfg(guest_arch = "x86_64")]
1226    #[clap(long)]
1227    pub intel_vtd: Vec<String>,
1228
1229    /// Attach a PCI Express root complex to the VM
1230    #[clap(long_help = r#"
1231Attach root complexes to the VM.
1232
1233Examples:
1234    # Attach root complex rc0 on segment 0 with bus and MMIO ranges
1235    --pcie-root-complex rc0,segment=0,start_bus=0,end_bus=255,low_mmio=4M,high_mmio=1G
1236
1237    # Configure HDM window size and restrictions (bitmask)
1238    --pcie-root-complex rc1,hdm=2G,hdm_window_restrictions=0x21
1239
1240Syntax: <name>[,opt=arg,...]
1241
1242Options:
1243    `segment=<value>`              configures the PCI Express segment, default 0
1244    `start_bus=<value>`            lowest valid bus number, default 0
1245    `end_bus=<value>`              highest valid bus number, default 255
1246    `low_mmio=<size>`              low MMIO window size, default 64M
1247    `high_mmio=<size>`             high MMIO window size, default 1G
1248    `low_mmio_base=<addr>`         pin low MMIO window base address (0x-prefixed hex)
1249    `high_mmio_base=<addr>`        pin high MMIO window base address (0x-prefixed hex)
1250    `hdm=<size>`                   HDM decoder MMIO window size (CFMWS window), default 1G
1251    `hdm_window_restrictions=<m>`  CFMWS window restriction bitmask (u16, decimal or 0x-prefixed hex),
1252                                   default DEVICE_COHERENT (bit 0, value 0x1)
1253    `preserve_bars`                keep pinned BARs at their assigned addresses
1254    `node=<value>`                 NUMA node the root complex is associated with
1255"#)]
1256    #[clap(long, conflicts_with("pcat"))]
1257    pub pcie_root_complex: Vec<PcieRootComplexCli>,
1258
1259    /// Place PCIe ECAM below 4 GiB for guest kernels that cannot discover high ECAM
1260    #[clap(long, requires("pcie_root_complex"), conflicts_with("pcat"))]
1261    pub pcie_ecam_below_4gb: bool,
1262
1263    /// Attach a PCI Express root port to the VM
1264    #[clap(long_help = r#"
1265Attach root ports to root complexes.
1266
1267Examples:
1268    # Attach root port rc0rp0 to root complex rc0
1269    --pcie-root-port rc0:rc0rp0
1270
1271    # Attach root port rc0rp1 to root complex rc0 with hotplug support
1272    --pcie-root-port rc0:rc0rp1,hotplug
1273
1274    # Attach root port rc0rp2 at device 5, function 0
1275    --pcie-root-port rc0:rc0rp2,addr=5
1276
1277    # Attach root port rc0rp3 at device 5, function 1
1278    --pcie-root-port rc0:rc0rp3,addr=5.1
1279
1280Syntax: <root_complex_name>:<name>[,opt,opt=arg,...]
1281
1282Options:
1283    `addr=<dev>[.<fn>]`            device/function to place this port at (default:
1284                                   lowest available); dev 0-31, fn 0-7
1285    `hotplug`                      enable hotplug support for this root port
1286    `acs=<mask>`                   ACS capability bitmask (u16, decimal or 0x-prefixed hex)
1287    `cxl`                          configure this root port as CXL-capable
1288    `pasid`                        configure this port to support PASID for downstream devices
1289"#)]
1290    #[clap(long, conflicts_with("pcat"))]
1291    pub pcie_root_port: Vec<PcieRootPortCli>,
1292
1293    /// Attach a PCI Express switch to the VM
1294    #[clap(long_help = r#"
1295Attach switches to root ports or downstream switch ports to create PCIe hierarchies.
1296
1297Examples:
1298    # Connect switch0 (with 4 downstream switch ports) directly to root port rp0
1299    --pcie-switch rp0:switch0,num_downstream_ports=4
1300
1301    # Connect switch1 (with 2 downstream switch ports) to downstream port 0 of switch0
1302    --pcie-switch switch0-downstream-0:switch1,num_downstream_ports=2
1303
1304    # Create a 3-level hierarchy: rp0 -> switch0 -> switch1 -> switch2
1305    --pcie-switch rp0:switch0
1306    --pcie-switch switch0-downstream-0:switch1
1307    --pcie-switch switch1-downstream-1:switch2
1308
1309    # Enable hotplug on all downstream switch ports of switch0
1310    --pcie-switch rp0:switch0,hotplug
1311
1312    # Enable PASID on all downstream switch ports of switch0
1313    --pcie-switch rp0:switch0,pasid
1314
1315Syntax: <port_name>:<name>[,opt,opt=arg,...]
1316
1317    port_name can be:
1318        - Root port name (e.g., "rp0") to connect directly to a root port
1319        - Downstream port name (e.g., "switch0-downstream-1") to connect to another switch
1320
1321Options:
1322    `hotplug`                       enable hotplug support for all downstream switch ports
1323    `num_downstream_ports=<value>`  number of downstream ports, default 4
1324    `acs=<mask>`                    ACS capability bitmask for downstream switch ports
1325    `pasid`                         configure this port to support PASID for downstream devices
1326"#)]
1327    #[clap(long, conflicts_with("pcat"))]
1328    pub pcie_switch: Vec<GenericPcieSwitchCli>,
1329
1330    /// Declare the device behind a PCIe port as an SRAT generic initiator
1331    #[clap(long_help = r#"
1332Declare that the device directly behind a PCIe port is a generic initiator
1333(GI) for a NUMA node, generating an SRAT Generic Initiator Affinity structure.
1334
1335The port may be a root port or a switch downstream port, so this works for
1336devices that sit behind a switch (e.g. a GPU placed under a switch shared
1337with a NIC for peer-to-peer DMA). The port is resolved by name against the
1338live topology after switch downstream ports are enumerated.
1339
1340Examples:
1341    # The device behind switch downstream port sw1-downstream-0 is a generic
1342    # initiator for NUMA node 1
1343    --pcie-generic-initiator port=sw1-downstream-0,node=1
1344
1345    # Also works for a root port name
1346    --pcie-generic-initiator port=rp0,node=2
1347
1348Syntax: port=<port_name>,node=<node>
1349"#)]
1350    #[clap(
1351        long = "pcie-generic-initiator",
1352        value_name = "port=<name>,node=<node>",
1353        conflicts_with("pcat")
1354    )]
1355    pub pcie_generic_initiator: Vec<PcieGenericInitiatorCli>,
1356
1357    /// Attach a PCIe remote device to a downstream port
1358    #[clap(long_help = r#"
1359Attach PCIe devices to root ports or downstream switch ports
1360which are implemented in a simulator running in a remote process.
1361
1362Examples:
1363    # Attach to root port rc0rp0 with default socket
1364    --pcie-remote rc0rp0
1365
1366    # Attach with custom socket address
1367    --pcie-remote rc0rp0,socket=0.0.0.0:48914
1368
1369    # Specify HU and controller identifiers
1370    --pcie-remote rc0rp0,hu=1,controller=0
1371
1372    # Multiple devices on different ports
1373    --pcie-remote rc0rp0,socket=0.0.0.0:48914
1374    --pcie-remote rc0rp1,socket=0.0.0.0:48915
1375
1376Syntax: <port_name>[,opt=arg,...]
1377
1378Options:
1379    `socket=<address>`              TCP socket (default: localhost:48914)
1380    `hu=<value>`                    Hardware unit identifier (default: 0)
1381    `controller=<value>`            Controller identifier (default: 0)
1382"#)]
1383    #[clap(long, conflicts_with("pcat"))]
1384    pub pcie_remote: Vec<PcieRemoteCli>,
1385
1386    /// Assign a host PCI device to the guest via VFIO (Linux only)
1387    #[clap(long_help = r#"
1388Assign a host PCI device to the guest via Linux VFIO.
1389
1390The device must be bound to vfio-pci on the host before starting the VM.
1391
1392Examples:
1393    --vfio host=0000:01:00.0,port=rp0
1394    --vfio host=0000:01:00.0,port=rp0,iommu=iommu0
1395
1396Keys:
1397    host=<pci_bdf>    (required) PCI address on the host
1398    port=<name>       (required) Root port or downstream switch port name
1399    iommu=<id>        (optional) Reference to an --iommu object. When present,
1400                      uses VFIO cdev + iommufd instead of the legacy group path.
1401"#)]
1402    #[cfg(target_os = "linux")]
1403    #[clap(long, conflicts_with("pcat"))]
1404    pub vfio: Vec<VfioDeviceCli>,
1405
1406    /// Create an iommufd context for VFIO cdev device assignment
1407    #[clap(long_help = r#"
1408Declare an iommufd context. Opens /dev/iommu so it can be referenced by
1409--vfio devices via the iommu=<id> key. The associated IOAS is allocated
1410the first time a --vfio device referring to this id is opened.
1411
1412Requires Linux kernel >= 6.6 with iommufd support.
1413
1414Examples:
1415    --iommu id=iommu0 --vfio host=0000:01:00.0,port=rp0,iommu=iommu0
1416
1417Syntax: id=<name>
1418"#)]
1419    #[cfg(target_os = "linux")]
1420    #[clap(long, conflicts_with("pcat"))]
1421    pub iommu: Vec<IommuCli>,
1422}
1423
1424impl Options {
1425    /// Returns the structured UEFI configuration with deprecated options merged in.
1426    pub fn effective_uefi(&self) -> anyhow::Result<Option<UefiCli>> {
1427        let mut uefi = match &self.uefi {
1428            Some(uefi) => uefi.clone(),
1429            None => return Ok(None),
1430        };
1431
1432        if let Some(firmware) = &self.deprecated_uefi_firmware {
1433            if uefi.firmware.is_some() {
1434                anyhow::bail!("--uefi firmware=... conflicts with --uefi-firmware");
1435            }
1436            uefi.firmware = Some(firmware.clone());
1437        }
1438        if let Some(console) = self.deprecated_uefi_console_mode {
1439            if uefi.console.is_some() {
1440                anyhow::bail!("--uefi console=... conflicts with --uefi-console-mode");
1441            }
1442            uefi.console = Some(console);
1443        }
1444        if let Some(diagnostics) = self.deprecated_efi_diagnostics_log_level {
1445            if uefi.diagnostics.is_some() {
1446                anyhow::bail!("--uefi diagnostics=... conflicts with --efi-diagnostics-log-level");
1447            }
1448            uefi.diagnostics = Some(diagnostics);
1449        }
1450
1451        uefi.debug |= self.deprecated_uefi_debug;
1452        uefi.enable_memory_protections |= self.deprecated_uefi_enable_memory_protections;
1453        uefi.force_dma_bounce |= self.deprecated_uefi_force_dma_bounce;
1454        uefi.force_firmware_version |= self.deprecated_uefi_force_firmware_version;
1455        uefi.disable_frontpage |= self.deprecated_disable_frontpage;
1456        uefi.default_boot_always_attempt |= self.deprecated_default_boot_always_attempt;
1457        Ok(Some(uefi))
1458    }
1459
1460    /// Returns the effective guest RAM size.
1461    pub fn memory_size(&self) -> u64 {
1462        self.memory.size.map(|m| m.0).unwrap_or(DEFAULT_MEMORY_SIZE)
1463    }
1464
1465    /// Returns whether guest RAM should be prefetched.
1466    pub fn prefetch_memory(&self) -> bool {
1467        self.memory.prefetch || self.deprecated_prefetch
1468    }
1469
1470    /// Returns whether guest RAM should use private anonymous backing.
1471    pub fn private_memory(&self) -> bool {
1472        self.memory.shared == Some(false) || self.deprecated_private_memory
1473    }
1474
1475    /// Returns whether guest RAM should be marked THP-eligible.
1476    pub fn transparent_hugepages(&self) -> bool {
1477        self.memory
1478            .transparent_hugepages
1479            .unwrap_or(!self.memory.hugepages)
1480            || self.deprecated_thp
1481    }
1482
1483    /// Returns the effective file backing path for guest RAM.
1484    pub fn memory_backing_file(&self) -> Option<&PathBuf> {
1485        self.memory
1486            .file
1487            .as_ref()
1488            .or(self.deprecated_memory_backing_file.as_ref())
1489    }
1490
1491    /// Validates combinations that span the new `--memory` parser and legacy aliases.
1492    ///
1493    /// Only checks that cannot be expressed elsewhere live here. Conflicts
1494    /// within a single `--memory` string are enforced by the parser, and
1495    /// semantic constraints (platform support, private-vs-shared, huge pages
1496    /// vs. legacy RAM, etc.) are enforced by the membacking builder at VM
1497    /// build time; those are not duplicated here.
1498    pub fn validate_memory_options(&self) -> anyhow::Result<()> {
1499        if self.memory.file.is_some() && self.deprecated_memory_backing_file.is_some() {
1500            anyhow::bail!("--memory file=... conflicts with --memory-backing-file");
1501        }
1502        if self.memory.file.is_some() && self.restore_snapshot.is_some() {
1503            anyhow::bail!("--memory file=... conflicts with --restore-snapshot");
1504        }
1505        if self.memory.shared == Some(true) && self.deprecated_private_memory {
1506            anyhow::bail!("--memory shared=on conflicts with --private-memory");
1507        }
1508        Ok(())
1509    }
1510
1511    /// Validates isolation-specific command-line option combinations.
1512    pub fn validate_isolation_options(&self) -> anyhow::Result<()> {
1513        if self.snp_restricted_injection && !matches!(self.isolation, Some(IsolationCli::Snp)) {
1514            anyhow::bail!("--snp-restricted-injection requires --isolation snp");
1515        }
1516        if self.snp_restricted_injection
1517            && self
1518                .hypervisor
1519                .as_deref()
1520                .and_then(|value| value.split(':').next())
1521                != Some("mshv")
1522        {
1523            anyhow::bail!("--snp-restricted-injection requires --hypervisor mshv");
1524        }
1525        if self.snp_restricted_injection
1526            && (self.uefi.is_some()
1527                || self.pcat
1528                || self.igvm.is_some()
1529                || self.restore_snapshot.is_some())
1530        {
1531            anyhow::bail!("--snp-restricted-injection requires Linux direct boot");
1532        }
1533        if matches!(self.isolation, Some(IsolationCli::Snp)) {
1534            if self.uefi.is_some() {
1535                anyhow::bail!("SNP isolation currently only supports Linux direct boot");
1536            }
1537            if self.memory.hugepages
1538                || self
1539                    .numa
1540                    .as_ref()
1541                    .is_some_and(|nodes| nodes.iter().any(|node| node.memory.hugepages))
1542            {
1543                anyhow::bail!("SNP isolation currently does not support hugetlb memory");
1544            }
1545        }
1546        Ok(())
1547    }
1548
1549    /// Validates IGVM personality selection.
1550    pub fn validate_igvm_options(&self) -> anyhow::Result<()> {
1551        let Some(igvm) = &self.igvm else {
1552            return Ok(());
1553        };
1554        match igvm.personality {
1555            IgvmPersonalityCli::Openhcl => {
1556                anyhow::ensure!(
1557                    self.hv && self.vtl2,
1558                    "--igvm personality=openhcl requires --hv --vtl2"
1559                );
1560            }
1561            IgvmPersonalityCli::Uefi | IgvmPersonalityCli::LinuxDirect => {
1562                anyhow::ensure!(
1563                    !self.vtl2,
1564                    "--vtl2 requires --igvm personality=openhcl for IGVM boots"
1565                );
1566                anyhow::ensure!(
1567                    self.uefi.is_none(),
1568                    "--uefi is only supported with --igvm personality=openhcl"
1569                );
1570            }
1571        }
1572        Ok(())
1573    }
1574}
1575
1576#[derive(Clone, Debug, PartialEq, vmm_cli::KeyValueArgs)]
1577pub struct IgvmCli {
1578    pub firmware: PathBuf,
1579    pub personality: IgvmPersonalityCli,
1580}
1581
1582#[derive(Clone, Debug, Default, vmm_cli::KeyValueArgs)]
1583pub struct UefiCli {
1584    pub firmware: Option<PathBuf>,
1585    #[kv(flag)]
1586    pub debug: bool,
1587    #[kv(flag)]
1588    pub enable_memory_protections: bool,
1589    #[kv(flag)]
1590    pub force_dma_bounce: bool,
1591    #[kv(flag)]
1592    pub force_firmware_version: bool,
1593    #[kv(flag)]
1594    pub disable_frontpage: bool,
1595    pub console: Option<UefiConsoleModeCli>,
1596    pub diagnostics: Option<EfiDiagnosticsLogLevelCli>,
1597    #[kv(flag)]
1598    pub default_boot_always_attempt: bool,
1599}
1600
1601/// Options for `--virtio-rtc`.
1602#[derive(Clone, Debug, PartialEq, Eq, vmm_cli::KeyValueArgs)]
1603pub struct VirtioRtcCli {
1604    /// Bus to attach the device to (auto | mmio | pci | pcie:PORT | vpci).
1605    #[kv(default = VirtioBusCli::Auto)]
1606    pub bus: VirtioBusCli,
1607}
1608
1609#[derive(Clone, Debug, PartialEq)]
1610pub struct FsArgs {
1611    pub tag: String,
1612    pub path: String,
1613    pub pcie_port: Option<String>,
1614}
1615
1616impl FromStr for FsArgs {
1617    type Err = anyhow::Error;
1618
1619    fn from_str(s: &str) -> Result<Self, Self::Err> {
1620        let (pcie_port, s) = parse_pcie_port_prefix(s);
1621        let mut s = s.split(',');
1622        let (Some(tag), Some(path), None) = (s.next(), s.next(), s.next()) else {
1623            anyhow::bail!("expected [pcie_port=<port>:]<tag>,<path>");
1624        };
1625        Ok(Self {
1626            tag: tag.to_owned(),
1627            path: path.to_owned(),
1628            pcie_port,
1629        })
1630    }
1631}
1632
1633#[derive(Clone, Debug, PartialEq)]
1634pub struct FsArgsWithOptions {
1635    /// The file system tag.
1636    pub tag: String,
1637    /// The root path.
1638    pub path: String,
1639    /// The extra options, joined with ';'.
1640    pub options: String,
1641    /// Optional PCIe port name.
1642    pub pcie_port: Option<String>,
1643}
1644
1645impl FromStr for FsArgsWithOptions {
1646    type Err = anyhow::Error;
1647
1648    fn from_str(s: &str) -> Result<Self, Self::Err> {
1649        let (pcie_port, s) = parse_pcie_port_prefix(s);
1650        let mut s = s.split(',');
1651        let (Some(tag), Some(path)) = (s.next(), s.next()) else {
1652            anyhow::bail!("expected [pcie_port=<port>:]<tag>,<path>[,<options>]");
1653        };
1654        let options = s.collect::<Vec<_>>().join(";");
1655        Ok(Self {
1656            tag: tag.to_owned(),
1657            path: path.to_owned(),
1658            options,
1659            pcie_port,
1660        })
1661    }
1662}
1663
1664/// What the VMM does on a guest power event (reset, power-off/hibernate,
1665/// triple-fault, or watchdog timeout). Parsed from `reset`, `halt`, `exit`, or
1666/// `exit:<code>`; a bare `exit` uses status 0.
1667#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1668pub enum GuestPowerAction {
1669    /// Restart the guest.
1670    Reset,
1671    /// Stop the VM but keep the VMM process, so it can be inspected or
1672    /// restarted from the REPL.
1673    Halt,
1674    /// Exit the VMM process with this status code.
1675    Exit(u8),
1676}
1677
1678/// Parse a [`GuestPowerAction`] from `reset`, `halt`, `exit`, or `exit:<code>`.
1679/// A bare `exit` exits with status 0; `exit:<code>` exits with `<code>` (0-255).
1680fn parse_guest_power_action(s: &str) -> Result<GuestPowerAction, String> {
1681    match s {
1682        "reset" => Ok(GuestPowerAction::Reset),
1683        "halt" => Ok(GuestPowerAction::Halt),
1684        "exit" => Ok(GuestPowerAction::Exit(0)),
1685        _ => match s.strip_prefix("exit:") {
1686            Some(code) => code
1687                .parse::<u8>()
1688                .map(GuestPowerAction::Exit)
1689                .map_err(|err| format!("invalid exit code '{code}' (expected 0-255): {err}")),
1690            None => Err(format!(
1691                "expected reset, halt, exit, or exit:<code>, got '{s}'"
1692            )),
1693        },
1694    }
1695}
1696
1697#[derive(Clone, Debug, PartialEq, Eq)]
1698pub enum VirtioBusCli {
1699    Auto,
1700    Mmio,
1701    Pci,
1702    Pcie(String),
1703    Vpci,
1704}
1705
1706impl FromStr for VirtioBusCli {
1707    type Err = String;
1708
1709    fn from_str(value: &str) -> Result<Self, Self::Err> {
1710        parse_virtio_bus(value, false)
1711    }
1712}
1713
1714fn parse_virtio_bus(value: &str, ignore_case: bool) -> Result<VirtioBusCli, String> {
1715    let eq = |value: &str, expected: &str| {
1716        if ignore_case {
1717            value.eq_ignore_ascii_case(expected)
1718        } else {
1719            value == expected
1720        }
1721    };
1722
1723    match value.split_once(':') {
1724        None if eq(value, "auto") => Ok(VirtioBusCli::Auto),
1725        None if eq(value, "mmio") => Ok(VirtioBusCli::Mmio),
1726        None if eq(value, "pci") => Ok(VirtioBusCli::Pci),
1727        None if eq(value, "vpci") => Ok(VirtioBusCli::Vpci),
1728        Some((bus, port)) if eq(bus, "pcie") && !port.is_empty() && !port.contains(':') => {
1729            Ok(VirtioBusCli::Pcie(port.to_string()))
1730        }
1731        _ => Err("expected auto, mmio, pci, pcie:<port>, or vpci".to_string()),
1732    }
1733}
1734
1735fn parse_virtio_vsock_bus(value: &str) -> Result<VirtioBusCli, String> {
1736    if value.eq_ignore_ascii_case("pcie") {
1737        return Ok(VirtioBusCli::Pcie("vsock".to_string()));
1738    }
1739
1740    match parse_virtio_bus(value, true) {
1741        Ok(bus @ (VirtioBusCli::Mmio | VirtioBusCli::Pci | VirtioBusCli::Pcie(_))) => Ok(bus),
1742        _ => Err("expected mmio, pci, pcie, or pcie:<port>".to_string()),
1743    }
1744}
1745
1746#[cfg(target_os = "linux")]
1747fn parse_vhost_vsock_cid(value: &str) -> Result<u32, String> {
1748    let cid = value
1749        .parse::<u32>()
1750        .map_err(|error| format!("invalid CID '{value}': {error}"))?;
1751    if !(3..u32::MAX).contains(&cid) {
1752        return Err(format!("CID must be between 3 and {}", u32::MAX - 1));
1753    }
1754    Ok(cid)
1755}
1756
1757/// Parse an optional `pcie_port=<name>:` prefix from a CLI argument string.
1758///
1759/// Returns `(Some(port_name), rest)` if the prefix is present, or
1760/// `(None, original)` if not.
1761fn parse_pcie_port_prefix(s: &str) -> (Option<String>, &str) {
1762    if let Some(rest) = s.strip_prefix("pcie_port=") {
1763        if let Some((port, rest)) = rest.split_once(':') {
1764            if !port.is_empty() {
1765                return (Some(port.to_string()), rest);
1766            }
1767        }
1768    }
1769    (None, s)
1770}
1771
1772#[derive(Clone, Debug, PartialEq)]
1773pub struct VirtioPmemArgs {
1774    pub path: String,
1775    pub pcie_port: Option<String>,
1776}
1777
1778impl FromStr for VirtioPmemArgs {
1779    type Err = anyhow::Error;
1780
1781    fn from_str(s: &str) -> Result<Self, Self::Err> {
1782        let (pcie_port, s) = parse_pcie_port_prefix(s);
1783        if s.is_empty() {
1784            anyhow::bail!("expected [pcie_port=<port>:]<path>");
1785        }
1786        Ok(Self {
1787            path: s.to_owned(),
1788            pcie_port,
1789        })
1790    }
1791}
1792
1793#[derive(clap::ValueEnum, Clone, Copy)]
1794pub enum SecureBootTemplateCli {
1795    Windows,
1796    UefiCa,
1797}
1798
1799/// TPM reference implementation version selected by `--tpm`.
1800#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
1801pub enum TpmVersionCli {
1802    /// TPM reference implementation version 1.38.
1803    #[value(name = "138", alias = "1.38")]
1804    V138,
1805    /// TPM reference implementation version 1.85.
1806    #[value(name = "185", alias = "1.85")]
1807    V185,
1808}
1809
1810fn parse_memory(s: &str) -> anyhow::Result<u64> {
1811    if s == "VMGS_DEFAULT" {
1812        Ok(vmgs_format::VMGS_DEFAULT_CAPACITY)
1813    } else {
1814        || -> Option<u64> {
1815            let mut b = s.as_bytes();
1816            if s.ends_with('B') {
1817                b = &b[..b.len() - 1]
1818            }
1819            if b.is_empty() {
1820                return None;
1821            }
1822            let multi = match b[b.len() - 1] as char {
1823                'T' => Some(1024 * 1024 * 1024 * 1024),
1824                'G' => Some(1024 * 1024 * 1024),
1825                'M' => Some(1024 * 1024),
1826                'K' => Some(1024),
1827                _ => None,
1828            };
1829            if multi.is_some() {
1830                b = &b[..b.len() - 1]
1831            }
1832            let n: u64 = std::str::from_utf8(b).ok()?.parse().ok()?;
1833            n.checked_mul(multi.unwrap_or(1))
1834        }()
1835        .with_context(|| format!("invalid memory size '{0}'", s))
1836    }
1837}
1838
1839/// Parses an address, which must be a `0x`-prefixed hexadecimal value.
1840fn parse_address(s: &str) -> anyhow::Result<u64> {
1841    let hex = s
1842        .strip_prefix("0x")
1843        .or_else(|| s.strip_prefix("0X"))
1844        .with_context(|| format!("invalid address '{s}', expected a 0x-prefixed hex value"))?;
1845    u64::from_str_radix(hex, 16).with_context(|| format!("invalid address '{s}'"))
1846}
1847
1848fn parse_acs_capability_mask(value: &str) -> anyhow::Result<u16> {
1849    if let Some(hex) = value
1850        .strip_prefix("0x")
1851        .or_else(|| value.strip_prefix("0X"))
1852    {
1853        u16::from_str_radix(hex, 16).context("invalid ACS capability mask")
1854    } else {
1855        value.parse::<u16>().context("invalid ACS capability mask")
1856    }
1857}
1858
1859/// A parsed SMBIOS `release=MAJOR.MINOR` value: exactly two `u8` components
1860/// separated by a dot.
1861#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1862pub struct SmbiosRelease(pub u8, pub u8);
1863
1864impl FromStr for SmbiosRelease {
1865    type Err = anyhow::Error;
1866
1867    fn from_str(value: &str) -> anyhow::Result<Self> {
1868        let (major, minor) = value
1869            .split_once('.')
1870            .with_context(|| format!("invalid smbios release '{value}', expected MAJOR.MINOR"))?;
1871        let major = major
1872            .parse::<u8>()
1873            .with_context(|| format!("invalid smbios release major '{major}'"))?;
1874        let minor = minor
1875            .parse::<u8>()
1876            .with_context(|| format!("invalid smbios release minor '{minor}'"))?;
1877        Ok(SmbiosRelease(major, minor))
1878    }
1879}
1880
1881/// A parsed SMBIOS `uuid=` value: either an explicit GUID or the literal
1882/// `random`, which requests a freshly generated per-VM GUID.
1883#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1884pub enum SmbiosUuid {
1885    /// Generate a fresh random GUID when the VM is created.
1886    Random,
1887    /// Use this fixed GUID.
1888    Fixed(Guid),
1889}
1890
1891impl FromStr for SmbiosUuid {
1892    type Err = anyhow::Error;
1893
1894    fn from_str(value: &str) -> anyhow::Result<Self> {
1895        if value == "random" {
1896            Ok(SmbiosUuid::Random)
1897        } else {
1898            let guid = value
1899                .parse::<Guid>()
1900                .with_context(|| format!("invalid smbios uuid '{value}'"))?;
1901            Ok(SmbiosUuid::Fixed(guid))
1902        }
1903    }
1904}
1905
1906/// SMBIOS Type 0 (BIOS Information) overrides parsed from a `--smbios type=0,…`
1907/// argument. Field names mirror `SmbiosBiosOverrides`.
1908#[derive(Debug, Clone, Default, PartialEq, Eq, vmm_cli::KeyValueArgs)]
1909pub struct SmbiosBiosCli {
1910    /// BIOS vendor (`vendor`).
1911    pub vendor: Option<String>,
1912    /// BIOS version (`version`).
1913    pub version: Option<String>,
1914    /// BIOS release date (`date`).
1915    #[kv(key = "date")]
1916    pub release_date: Option<String>,
1917    /// System BIOS Major/Minor Release (`release=MAJOR.MINOR`).
1918    pub release: Option<SmbiosRelease>,
1919}
1920
1921/// SMBIOS Type 1 (System Information) overrides parsed from a `--smbios type=1,…`
1922/// argument. Field names mirror `SmbiosSystemOverrides`.
1923#[derive(Debug, Clone, Default, PartialEq, Eq, vmm_cli::KeyValueArgs)]
1924pub struct SmbiosSystemCli {
1925    /// System manufacturer (`manufacturer`).
1926    pub manufacturer: Option<String>,
1927    /// System product name (`product`).
1928    #[kv(key = "product")]
1929    pub product_name: Option<String>,
1930    /// System version (`version`).
1931    pub version: Option<String>,
1932    /// System serial number (`serial`).
1933    #[kv(key = "serial")]
1934    pub serial_number: Option<String>,
1935    /// System SKU number (`sku`).
1936    #[kv(key = "sku")]
1937    pub sku_number: Option<String>,
1938    /// System family (`family`).
1939    pub family: Option<String>,
1940    /// System UUID (`uuid=GUID` or `uuid=random`).
1941    pub uuid: Option<SmbiosUuid>,
1942}
1943
1944/// SMBIOS (DMI) identity overrides parsed from `--smbios` arguments, grouped by
1945/// SMBIOS structure type to mirror the required `type=N` CLI prefix and the
1946/// loader's `SmbiosConfig` layout.
1947///
1948/// Each `--smbios` argument targets exactly one type; multiple arguments are
1949/// merged with [`SmbiosCli::merge`].
1950#[derive(Debug, Clone, Default, PartialEq, Eq)]
1951pub struct SmbiosCli {
1952    /// Type 0 (BIOS Information) overrides.
1953    pub bios: SmbiosBiosCli,
1954    /// Type 1 (System Information) overrides.
1955    pub system: SmbiosSystemCli,
1956}
1957
1958/// Set an override slot, erroring if it is already set.
1959fn smbios_set_once<T>(slot: &mut Option<T>, key: &str, value: T) -> anyhow::Result<()> {
1960    anyhow::ensure!(slot.is_none(), "duplicate smbios option '{key}'");
1961    *slot = Some(value);
1962    Ok(())
1963}
1964
1965impl SmbiosCli {
1966    /// Merge another parsed `--smbios` argument into this one, erroring if any
1967    /// field is set by both.
1968    pub fn merge(&mut self, other: SmbiosCli) -> anyhow::Result<()> {
1969        let SmbiosCli {
1970            bios:
1971                SmbiosBiosCli {
1972                    vendor,
1973                    version: bios_version,
1974                    release_date,
1975                    release,
1976                },
1977            system:
1978                SmbiosSystemCli {
1979                    manufacturer,
1980                    product_name,
1981                    version: system_version,
1982                    serial_number,
1983                    sku_number,
1984                    family,
1985                    uuid,
1986                },
1987        } = other;
1988        merge_smbios_field(&mut self.bios.vendor, vendor, 0, "vendor")?;
1989        merge_smbios_field(&mut self.bios.version, bios_version, 0, "version")?;
1990        merge_smbios_field(&mut self.bios.release_date, release_date, 0, "date")?;
1991        merge_smbios_field(&mut self.bios.release, release, 0, "release")?;
1992        merge_smbios_field(
1993            &mut self.system.manufacturer,
1994            manufacturer,
1995            1,
1996            "manufacturer",
1997        )?;
1998        merge_smbios_field(&mut self.system.product_name, product_name, 1, "product")?;
1999        merge_smbios_field(&mut self.system.version, system_version, 1, "version")?;
2000        merge_smbios_field(&mut self.system.serial_number, serial_number, 1, "serial")?;
2001        merge_smbios_field(&mut self.system.sku_number, sku_number, 1, "sku")?;
2002        merge_smbios_field(&mut self.system.family, family, 1, "family")?;
2003        merge_smbios_field(&mut self.system.uuid, uuid, 1, "uuid")?;
2004        Ok(())
2005    }
2006}
2007
2008/// Merge a single override field, erroring if both sides are set. `typ` is the
2009/// SMBIOS structure type the field belongs to, included in the error message.
2010fn merge_smbios_field<T>(
2011    dst: &mut Option<T>,
2012    src: Option<T>,
2013    typ: u8,
2014    key: &str,
2015) -> anyhow::Result<()> {
2016    if let Some(value) = src {
2017        smbios_set_once(dst, &format!("type={typ} {key}"), value)?;
2018    }
2019    Ok(())
2020}
2021
2022/// Parse a single `--smbios` argument: `type=N,key=value[,key=value...]`.
2023///
2024/// `type=N` is required (there is no default) and selects the key namespace;
2025/// the remaining `key=value` pairs are parsed by the matching per-type
2026/// `KeyValueArgs` struct. Unknown types and unknown keys are hard errors (no
2027/// silent drop).
2028fn parse_smbios(s: &str) -> anyhow::Result<SmbiosCli> {
2029    let mut typ: Option<u8> = None;
2030    let mut rest = Vec::new();
2031    for part in s.split(',') {
2032        let (key, value) = part
2033            .split_once('=')
2034            .with_context(|| format!("invalid smbios option '{part}', expected key=value"))?;
2035        if key.is_empty() || value.is_empty() {
2036            anyhow::bail!("invalid smbios option '{part}', expected key=value");
2037        }
2038        if key == "type" {
2039            anyhow::ensure!(typ.is_none(), "duplicate smbios option 'type'");
2040            typ = Some(
2041                value
2042                    .parse::<u8>()
2043                    .with_context(|| format!("invalid smbios type '{value}'"))?,
2044            );
2045        } else {
2046            rest.push(part);
2047        }
2048    }
2049
2050    let typ = typ.context("smbios option requires 'type=N' (e.g. 'type=1')")?;
2051    let rest = rest.join(",");
2052    Ok(match typ {
2053        0 => SmbiosCli {
2054            bios: rest.parse()?,
2055            ..Default::default()
2056        },
2057        1 => SmbiosCli {
2058            system: rest.parse()?,
2059            ..Default::default()
2060        },
2061        other => anyhow::bail!("unsupported smbios type '{other}' (expected 0 or 1)"),
2062    })
2063}
2064
2065fn parse_memory_config(s: &str) -> anyhow::Result<MemoryCli> {
2066    // Bare shortcut: `--memory 64G` sets only the size.
2067    let memory = if !s.contains('=') && !s.contains(',') {
2068        MemoryCli {
2069            size: Some(s.parse::<vmm_cli::MemorySize>()?),
2070            ..Default::default()
2071        }
2072    } else {
2073        s.parse::<MemoryCli>()?
2074    };
2075    memory.validate()?;
2076    Ok(memory)
2077}
2078
2079fn parse_numa_node(s: &str) -> anyhow::Result<NumaNodeCli> {
2080    let node: NumaNodeCli = s.parse()?;
2081    anyhow::ensure!(
2082        node.memory.size.is_some(),
2083        "numa node requires 'size' option"
2084    );
2085    anyhow::ensure!(
2086        node.memory.file.is_none(),
2087        "'file' is not supported in --numa"
2088    );
2089    node.memory.validate()?;
2090    Ok(node)
2091}
2092
2093fn parse_numa_distance(s: &str) -> anyhow::Result<NumaDistanceCli> {
2094    let parts: Vec<&str> = s.split(':').collect();
2095    anyhow::ensure!(
2096        parts.len() == 3,
2097        "expected SRC:DST:DISTANCE format, got '{s}'"
2098    );
2099    let src = parts[0].parse::<u32>().context("invalid source node")?;
2100    let dst = parts[1]
2101        .parse::<u32>()
2102        .context("invalid destination node")?;
2103    let distance = parts[2].parse::<u8>().context("invalid distance")?;
2104    anyhow::ensure!(
2105        distance >= 10,
2106        "distance must be >= 10 (10 = local), got {distance}"
2107    );
2108    Ok(NumaDistanceCli { src, dst, distance })
2109}
2110
2111/// Parse a number from a string that could be prefixed with 0x to indicate hex.
2112fn parse_number(s: &str) -> Result<u64, std::num::ParseIntError> {
2113    match s.strip_prefix("0x") {
2114        Some(rest) => u64::from_str_radix(rest, 16),
2115        None => s.parse::<u64>(),
2116    }
2117}
2118
2119#[derive(Clone, Debug, PartialEq)]
2120pub enum DiskCliKind {
2121    // mem:<len>
2122    Memory(u64),
2123    // memdiff:<kind>
2124    MemoryDiff(Box<DiskCliKind>),
2125    // sql:<path>[;create=<len>]
2126    Sqlite {
2127        path: PathBuf,
2128        create_with_len: Option<u64>,
2129    },
2130    // sqldiff:<path>[;create]:<kind>
2131    SqliteDiff {
2132        path: PathBuf,
2133        create: bool,
2134        disk: Box<DiskCliKind>,
2135    },
2136    // autocache:[key]:<kind>
2137    AutoCacheSqlite {
2138        cache_path: String,
2139        key: Option<String>,
2140        disk: Box<DiskCliKind>,
2141    },
2142    // prwrap:<kind>
2143    PersistentReservationsWrapper(Box<DiskCliKind>),
2144    // file:<path>[;direct][;create=<len>]
2145    File {
2146        path: PathBuf,
2147        create_with_len: Option<u64>,
2148        direct: bool,
2149    },
2150    // blob:<type>:<url>
2151    Blob {
2152        kind: BlobKind,
2153        url: String,
2154    },
2155    // crypt:<cipher>:<key_file>:<kind>
2156    Crypt {
2157        cipher: DiskCipher,
2158        key_file: PathBuf,
2159        disk: Box<DiskCliKind>,
2160    },
2161    // delay:<delay_ms>:<kind>
2162    DelayDiskWrapper {
2163        delay_ms: u64,
2164        disk: Box<DiskCliKind>,
2165    },
2166}
2167
2168#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]
2169pub enum DiskCipher {
2170    #[clap(name = "xts-aes-256")]
2171    XtsAes256,
2172}
2173
2174#[derive(Copy, Clone, Debug, PartialEq)]
2175pub enum BlobKind {
2176    Flat,
2177    Vhd1,
2178}
2179
2180struct FileOpts {
2181    path: PathBuf,
2182    create_with_len: Option<u64>,
2183    direct: bool,
2184}
2185
2186fn parse_file_opts(arg: &str) -> anyhow::Result<FileOpts> {
2187    let mut path = arg;
2188    let mut create_with_len = None;
2189    let mut direct = false;
2190
2191    // Parse semicolon-delimited options after the path.
2192    if let Some((p, rest)) = arg.split_once(';') {
2193        path = p;
2194        for opt in rest.split(';') {
2195            if let Some(len) = opt.strip_prefix("create=") {
2196                create_with_len = Some(parse_memory(len)?);
2197            } else if opt == "direct" {
2198                direct = true;
2199            } else {
2200                anyhow::bail!("invalid file option '{opt}', expected 'create=<len>' or 'direct'");
2201            }
2202        }
2203    }
2204
2205    Ok(FileOpts {
2206        path: path.into(),
2207        create_with_len,
2208        direct,
2209    })
2210}
2211
2212impl DiskCliKind {
2213    /// Parse an `autocache:[key]:<kind>` disk spec, given the cache path
2214    /// (normally read from `OPENVMM_AUTO_CACHE_PATH`).
2215    fn parse_autocache(
2216        arg: &str,
2217        cache_path: Result<String, std::env::VarError>,
2218    ) -> anyhow::Result<Self> {
2219        let (key, kind) = arg.split_once(':').context("expected [key]:kind")?;
2220        let cache_path = cache_path.context("must set cache path via OPENVMM_AUTO_CACHE_PATH")?;
2221        Ok(DiskCliKind::AutoCacheSqlite {
2222            cache_path,
2223            key: (!key.is_empty()).then(|| key.to_string()),
2224            disk: Box::new(kind.parse()?),
2225        })
2226    }
2227}
2228
2229impl FromStr for DiskCliKind {
2230    type Err = anyhow::Error;
2231
2232    fn from_str(s: &str) -> anyhow::Result<Self> {
2233        let disk = match s.split_once(':') {
2234            // convenience support for passing bare paths as file disks
2235            None => {
2236                let FileOpts {
2237                    path,
2238                    create_with_len,
2239                    direct,
2240                } = parse_file_opts(s)?;
2241                DiskCliKind::File {
2242                    path,
2243                    create_with_len,
2244                    direct,
2245                }
2246            }
2247            Some((kind, arg)) => match kind {
2248                "mem" => DiskCliKind::Memory(parse_memory(arg)?),
2249                "memdiff" => DiskCliKind::MemoryDiff(Box::new(arg.parse()?)),
2250                "sql" => {
2251                    let FileOpts {
2252                        path,
2253                        create_with_len,
2254                        direct,
2255                    } = parse_file_opts(arg)?;
2256                    if direct {
2257                        anyhow::bail!("'direct' is not supported for 'sql' disks");
2258                    }
2259                    DiskCliKind::Sqlite {
2260                        path,
2261                        create_with_len,
2262                    }
2263                }
2264                "sqldiff" => {
2265                    let (path_and_opts, kind) =
2266                        arg.split_once(':').context("expected path[;opts]:kind")?;
2267                    let disk = Box::new(kind.parse()?);
2268                    match path_and_opts.split_once(';') {
2269                        Some((path, create)) => {
2270                            if create != "create" {
2271                                anyhow::bail!("invalid syntax after ';', expected 'create'")
2272                            }
2273                            DiskCliKind::SqliteDiff {
2274                                path: path.into(),
2275                                create: true,
2276                                disk,
2277                            }
2278                        }
2279                        None => DiskCliKind::SqliteDiff {
2280                            path: path_and_opts.into(),
2281                            create: false,
2282                            disk,
2283                        },
2284                    }
2285                }
2286                "autocache" => {
2287                    Self::parse_autocache(arg, std::env::var("OPENVMM_AUTO_CACHE_PATH"))?
2288                }
2289                "prwrap" => DiskCliKind::PersistentReservationsWrapper(Box::new(arg.parse()?)),
2290                "delay" => {
2291                    let (delay_ms, kind) = arg.split_once(':').context("expected delay_ms:kind")?;
2292                    DiskCliKind::DelayDiskWrapper {
2293                        delay_ms: delay_ms.parse().context("invalid disk delay")?,
2294                        disk: Box::new(kind.parse()?),
2295                    }
2296                }
2297                "file" => {
2298                    let FileOpts {
2299                        path,
2300                        create_with_len,
2301                        direct,
2302                    } = parse_file_opts(arg)?;
2303                    DiskCliKind::File {
2304                        path,
2305                        create_with_len,
2306                        direct,
2307                    }
2308                }
2309                "blob" => {
2310                    let (blob_kind, url) = arg.split_once(':').context("expected kind:url")?;
2311                    let blob_kind = match blob_kind {
2312                        "flat" => BlobKind::Flat,
2313                        "vhd1" => BlobKind::Vhd1,
2314                        _ => anyhow::bail!("unknown blob kind {blob_kind}"),
2315                    };
2316                    DiskCliKind::Blob {
2317                        kind: blob_kind,
2318                        url: url.to_string(),
2319                    }
2320                }
2321                "crypt" => {
2322                    let (cipher, (key, kind)) = arg
2323                        .split_once(':')
2324                        .and_then(|(cipher, arg)| Some((cipher, arg.split_once(':')?)))
2325                        .context("expected cipher:key_file:kind")?;
2326                    DiskCliKind::Crypt {
2327                        cipher: ValueEnum::from_str(cipher, false)
2328                            .map_err(|err| anyhow::anyhow!("invalid cipher: {err}"))?,
2329                        key_file: PathBuf::from(key),
2330                        disk: Box::new(kind.parse()?),
2331                    }
2332                }
2333                kind => {
2334                    // here's a fun edge case: what if the user passes `--disk d:\path\to\disk.img`?
2335                    //
2336                    // in this case, we actually want to treat that leading `d:` as part of the
2337                    // path, rather than as a disk with `kind == 'd'`
2338                    let FileOpts {
2339                        path,
2340                        create_with_len,
2341                        direct,
2342                    } = parse_file_opts(s)?;
2343                    if path.has_root() {
2344                        DiskCliKind::File {
2345                            path,
2346                            create_with_len,
2347                            direct,
2348                        }
2349                    } else {
2350                        anyhow::bail!("invalid disk kind {kind}");
2351                    }
2352                }
2353            },
2354        };
2355        Ok(disk)
2356    }
2357}
2358
2359/// Wire transport selection for `--rpc`.
2360#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
2361pub enum RpcTransportCli {
2362    /// Auto-detect ttrpc vs. gRPC per connection, based on the first byte of
2363    /// the stream.
2364    #[default]
2365    Auto,
2366    /// Accept ttrpc clients only.
2367    Ttrpc,
2368    /// Accept gRPC clients only.
2369    Grpc,
2370}
2371
2372/// RPC server configuration parsed from `--rpc`.
2373#[derive(Clone, Debug, PartialEq, Eq)]
2374pub struct RpcCli {
2375    /// Unix socket path to listen on.
2376    pub path: PathBuf,
2377    /// Wire transport to accept.
2378    pub transport: RpcTransportCli,
2379}
2380
2381impl FromStr for RpcCli {
2382    type Err = anyhow::Error;
2383
2384    fn from_str(s: &str) -> anyhow::Result<Self> {
2385        let mut path = None;
2386        let mut transport = None;
2387        for part in s.split(',') {
2388            let (key, value) = part
2389                .split_once('=')
2390                .with_context(|| format!("invalid rpc option '{part}', expected key=value"))?;
2391            match key {
2392                "path" => {
2393                    anyhow::ensure!(path.is_none(), "duplicate option 'path'");
2394                    anyhow::ensure!(!value.is_empty(), "'path' requires a value");
2395                    path = Some(PathBuf::from(value));
2396                }
2397                "transport" => {
2398                    anyhow::ensure!(transport.is_none(), "duplicate option 'transport'");
2399                    transport = Some(match value {
2400                        "auto" => RpcTransportCli::Auto,
2401                        "ttrpc" => RpcTransportCli::Ttrpc,
2402                        "grpc" => RpcTransportCli::Grpc,
2403                        _ => anyhow::bail!(
2404                            "invalid transport '{value}', expected auto, ttrpc, or grpc"
2405                        ),
2406                    });
2407                }
2408                _ => anyhow::bail!("unknown rpc option '{key}'"),
2409            }
2410        }
2411
2412        Ok(RpcCli {
2413            path: path.context("'path' is required")?,
2414            transport: transport.unwrap_or_default(),
2415        })
2416    }
2417}
2418
2419#[derive(Clone)]
2420pub struct VmgsCli {
2421    pub kind: DiskCliKind,
2422    pub provision: ProvisionVmgs,
2423}
2424
2425#[derive(Copy, Clone)]
2426pub enum ProvisionVmgs {
2427    OnEmpty,
2428    OnFailure,
2429    True,
2430}
2431
2432impl FromStr for VmgsCli {
2433    type Err = anyhow::Error;
2434
2435    fn from_str(s: &str) -> anyhow::Result<Self> {
2436        let (kind, opt) = s
2437            .split_once(',')
2438            .map(|(k, o)| (k, Some(o)))
2439            .unwrap_or((s, None));
2440        let kind = kind.parse()?;
2441
2442        let provision = match opt {
2443            None => ProvisionVmgs::OnEmpty,
2444            Some("fmt-on-fail") => ProvisionVmgs::OnFailure,
2445            Some("fmt") => ProvisionVmgs::True,
2446            Some(opt) => anyhow::bail!("unknown option: '{opt}'"),
2447        };
2448
2449        Ok(VmgsCli { kind, provision })
2450    }
2451}
2452
2453/// VNC server configuration options.
2454#[derive(clap::Args)]
2455pub struct VncCli {
2456    /// Listen for VNC connections. Implied by --gfx.
2457    #[clap(long)]
2458    pub vnc: bool,
2459
2460    /// VNC port number
2461    #[clap(long, value_name = "PORT", default_value = "5900")]
2462    pub vnc_port: u16,
2463
2464    /// VNC listen address (use 0.0.0.0 for all IPv4, :: for dual-stack IPv4+IPv6).
2465    /// Accepts a bare IP address (combined with --vnc-port) or a full socket
2466    /// address like [::1]:5900 (overrides --vnc-port).
2467    #[clap(long, value_name = "ADDRESS", default_value = "127.0.0.1")]
2468    pub vnc_listen: String,
2469
2470    /// Maximum concurrent VNC clients (~8MB memory per client for framebuffer buffers)
2471    #[clap(long, value_name = "COUNT", default_value = "16")]
2472    pub vnc_max_clients: usize,
2473
2474    /// When the client limit is reached, disconnect the oldest client
2475    /// instead of rejecting the new connection
2476    #[clap(long)]
2477    pub vnc_evict_oldest: bool,
2478}
2479
2480// <kind>[,ro]
2481#[derive(Clone)]
2482pub struct DiskCli {
2483    pub vtl: DeviceVtl,
2484    pub kind: DiskCliKind,
2485    pub read_only: bool,
2486    pub is_dvd: bool,
2487    pub underhill: Option<UnderhillDiskSource>,
2488    pub pcie_port: Option<String>,
2489    pub serial: Option<String>,
2490    pub controller: Option<String>,
2491    pub nsid: Option<u32>,
2492    pub lun: Option<u8>,
2493    pub relay: Option<(String, Option<u32>)>,
2494}
2495
2496#[derive(Copy, Clone)]
2497pub enum UnderhillDiskSource {
2498    Scsi,
2499    Nvme,
2500}
2501
2502/// A `relay=<name>[:<location>]` target for an OpenHCL-managed controller.
2503struct RelayTarget {
2504    name: String,
2505    location: Option<u32>,
2506}
2507
2508impl FromStr for RelayTarget {
2509    type Err = anyhow::Error;
2510
2511    fn from_str(s: &str) -> anyhow::Result<Self> {
2512        Ok(if let Some((name, loc)) = s.split_once(':') {
2513            RelayTarget {
2514                name: name.to_string(),
2515                location: Some(loc.parse::<u32>().context("invalid relay location")?),
2516            }
2517        } else {
2518            RelayTarget {
2519                name: s.to_string(),
2520                location: None,
2521            }
2522        })
2523    }
2524}
2525
2526/// Raw `--disk`/`--nvme`/`--virtio-blk` options, resolved and validated into a
2527/// [`DiskCli`] by its `FromStr`.
2528#[derive(vmm_cli::KeyValueArgs)]
2529struct DiskArgs {
2530    #[kv(positional)]
2531    kind: DiskCliKind,
2532    #[kv(flag)]
2533    ro: bool,
2534    #[kv(flag)]
2535    dvd: bool,
2536    #[kv(flag, key = "vtl2", present = DeviceVtl::Vtl2, absent = DeviceVtl::Vtl0)]
2537    vtl: DeviceVtl,
2538    #[kv(flag)]
2539    uh: bool,
2540    #[kv(flag, key = "uh-nvme")]
2541    uh_nvme: bool,
2542    pcie_port: Option<String>,
2543    serial: Option<String>,
2544    #[kv(key = "on")]
2545    controller: Option<String>,
2546    nsid: Option<u32>,
2547    lun: Option<u8>,
2548    relay: Option<RelayTarget>,
2549}
2550
2551impl FromStr for DiskCli {
2552    type Err = anyhow::Error;
2553
2554    fn from_str(s: &str) -> anyhow::Result<Self> {
2555        let args: DiskArgs = s.parse()?;
2556
2557        let underhill = match (args.uh, args.uh_nvme) {
2558            (false, false) => None,
2559            (true, false) => Some(UnderhillDiskSource::Scsi),
2560            (false, true) => Some(UnderhillDiskSource::Nvme),
2561            (true, true) => anyhow::bail!("`uh` and `uh-nvme` are mutually exclusive"),
2562        };
2563        let read_only = args.ro || args.dvd;
2564        let is_dvd = args.dvd;
2565        let vtl = args.vtl;
2566        let pcie_port = args.pcie_port;
2567        let serial = args.serial;
2568        let controller = args.controller;
2569        let nsid = args.nsid;
2570        let lun = args.lun;
2571        let relay = args.relay.map(|r| (r.name, r.location));
2572
2573        if serial.as_ref().is_some_and(|serial| {
2574            serial.bytes().any(|byte| {
2575                !(byte.is_ascii_graphic() || byte == b' ') || matches!(byte, b'[' | b']')
2576            })
2577        }) {
2578            anyhow::bail!("`serial` must contain only printable ASCII characters and no brackets");
2579        }
2580
2581        if underhill.is_some() && vtl != DeviceVtl::Vtl0 {
2582            anyhow::bail!("`uh` or `uh-nvme` is incompatible with `vtl2`");
2583        }
2584
2585        if pcie_port.is_some() && (underhill.is_some() || vtl != DeviceVtl::Vtl0 || is_dvd) {
2586            anyhow::bail!("`pcie_port` is incompatible with `uh`, `uh-nvme`, `vtl2`, and `dvd`");
2587        }
2588
2589        if controller.is_some() && pcie_port.is_some() {
2590            anyhow::bail!("`on` is incompatible with `pcie_port`");
2591        }
2592
2593        if controller.is_some() && vtl != DeviceVtl::Vtl0 {
2594            anyhow::bail!(
2595                "`vtl2` is incompatible with `on`; the controller's VTL determines placement"
2596            );
2597        }
2598
2599        if controller.is_some() && underhill.is_some() {
2600            anyhow::bail!("`on` is incompatible with `uh` and `uh-nvme`; use `relay` instead");
2601        }
2602
2603        if nsid.is_some() && controller.is_none() {
2604            anyhow::bail!("`nsid` requires `on`");
2605        }
2606
2607        if lun.is_some() && controller.is_none() {
2608            anyhow::bail!("`lun` requires `on`");
2609        }
2610
2611        if nsid.is_some() && lun.is_some() {
2612            anyhow::bail!("`nsid` and `lun` are mutually exclusive");
2613        }
2614
2615        if relay.is_some() && controller.is_none() {
2616            anyhow::bail!("`relay` requires `on`");
2617        }
2618
2619        if relay.is_some() && underhill.is_some() {
2620            anyhow::bail!("`relay` is incompatible with `uh` and `uh-nvme`");
2621        }
2622
2623        Ok(DiskCli {
2624            vtl,
2625            kind: args.kind,
2626            read_only,
2627            is_dvd,
2628            underhill,
2629            pcie_port,
2630            serial,
2631            controller,
2632            nsid,
2633            lun,
2634            relay,
2635        })
2636    }
2637}
2638
2639/// The transport for a named NVMe controller.
2640#[derive(Clone, Debug, PartialEq, vmm_cli::KeyValueGroup)]
2641pub enum NvmeControllerTransport {
2642    /// Present via PCIe on the specified root port.
2643    #[kv(key = "pcie_port")]
2644    Pcie(String),
2645    /// Present via VPCI with an optional instance GUID.
2646    #[kv(key = "vpci")]
2647    Vpci(Option<Guid>),
2648}
2649
2650/// CLI arguments for a named NVMe controller.
2651#[derive(Clone, Debug, vmm_cli::KeyValueArgs)]
2652pub struct NvmeControllerCli {
2653    /// Controller name, referenced by `--disk on=<name>`.
2654    pub id: String,
2655    /// Transport configuration.
2656    #[kv(flatten)]
2657    pub transport: NvmeControllerTransport,
2658    /// VTL assignment (default VTL0).
2659    #[kv(flag, key = "vtl2", present = DeviceVtl::Vtl2, absent = DeviceVtl::Vtl0)]
2660    pub vtl: DeviceVtl,
2661}
2662
2663/// CLI arguments for a named VMBus SCSI controller.
2664#[derive(Clone, Debug, vmm_cli::KeyValueArgs)]
2665pub struct ScsiControllerCli {
2666    /// Controller name, referenced by `--disk on=<name>`.
2667    pub id: String,
2668    /// Number of sub-channels.
2669    #[kv(default)]
2670    pub sub_channels: u16,
2671    /// VTL assignment (default VTL0).
2672    #[kv(flag, key = "vtl2", present = DeviceVtl::Vtl2, absent = DeviceVtl::Vtl0)]
2673    pub vtl: DeviceVtl,
2674}
2675
2676/// Protocol type for an OpenHCL-managed controller.
2677#[derive(Copy, Clone, Debug, PartialEq)]
2678pub enum OpenhclControllerType {
2679    Scsi,
2680    Nvme,
2681}
2682
2683/// CLI arguments for an OpenHCL-managed storage controller (relay target).
2684#[derive(Clone, Debug, vmm_cli::KeyValueArgs)]
2685pub struct OpenhclControllerCli {
2686    /// Controller name, referenced by `--disk ... relay=<name>`.
2687    pub id: String,
2688    /// Controller protocol.
2689    #[kv(key = "type")]
2690    pub controller_type: OpenhclControllerType,
2691    /// Instance GUID (auto-derived from name if omitted).
2692    pub guid: Option<Guid>,
2693}
2694
2695impl FromStr for OpenhclControllerType {
2696    type Err = anyhow::Error;
2697
2698    fn from_str(s: &str) -> anyhow::Result<Self> {
2699        Ok(match s {
2700            "scsi" => OpenhclControllerType::Scsi,
2701            "nvme" => OpenhclControllerType::Nvme,
2702            other => anyhow::bail!("unknown controller type: '{other}'"),
2703        })
2704    }
2705}
2706
2707/// CLI arguments for a CXL Type-3 test endpoint.
2708#[derive(Clone, Debug, PartialEq)]
2709pub struct CxlTestDeviceCli {
2710    /// Size of HDM memory the test device should expose and back.
2711    pub hdm_size: u64,
2712    /// PCIe root port name where the device is attached.
2713    pub pcie_port: String,
2714}
2715
2716impl FromStr for CxlTestDeviceCli {
2717    type Err = anyhow::Error;
2718
2719    fn from_str(s: &str) -> anyhow::Result<Self> {
2720        let mut opts = s.split(',');
2721        let first = opts.next().context("expected CXL test device config")?;
2722        let (kind, arg) = first
2723            .split_once(':')
2724            .context("expected CXL test syntax: mem:<len>")?;
2725
2726        if kind != "mem" {
2727            anyhow::bail!("unsupported CXL test backing kind '{kind}', expected 'mem'");
2728        }
2729
2730        let hdm_size = parse_memory(arg).context("failed to parse CXL test HDM size")?;
2731        let mut pcie_port = None;
2732
2733        for opt in opts {
2734            let mut kv = opt.split('=');
2735            let key = kv.next().unwrap_or_default();
2736            match key {
2737                "pcie_port" => {
2738                    let val = kv.next();
2739                    if val.is_none_or(|v| v.is_empty()) {
2740                        anyhow::bail!("`pcie_port` requires a port name");
2741                    }
2742                    pcie_port = Some(val.unwrap().to_string());
2743                }
2744                _ => anyhow::bail!("unknown option: '{opt}'"),
2745            }
2746        }
2747
2748        let Some(pcie_port) = pcie_port else {
2749            anyhow::bail!("`pcie_port=<name>` is required for `--cxl-test`");
2750        };
2751
2752        Ok(Self {
2753            hdm_size,
2754            pcie_port,
2755        })
2756    }
2757}
2758
2759// <kind>[,ro,s]
2760#[derive(Clone)]
2761pub struct IdeDiskCli {
2762    pub kind: DiskCliKind,
2763    pub read_only: bool,
2764    pub channel: Option<u8>,
2765    pub device: Option<u8>,
2766    pub is_dvd: bool,
2767}
2768
2769impl FromStr for IdeDiskCli {
2770    type Err = anyhow::Error;
2771
2772    fn from_str(s: &str) -> anyhow::Result<Self> {
2773        let mut opts = s.split(',');
2774        let kind = opts.next().unwrap().parse()?;
2775
2776        let mut read_only = false;
2777        let mut channel = None;
2778        let mut device = None;
2779        let mut is_dvd = false;
2780        for opt in opts {
2781            let mut s = opt.split('=');
2782            let opt = s.next().unwrap();
2783            match opt {
2784                "ro" => read_only = true,
2785                "p" => channel = Some(0),
2786                "s" => channel = Some(1),
2787                "0" => device = Some(0),
2788                "1" => device = Some(1),
2789                "dvd" => {
2790                    is_dvd = true;
2791                    read_only = true;
2792                }
2793                _ => anyhow::bail!("unknown option: '{opt}'"),
2794            }
2795        }
2796
2797        Ok(IdeDiskCli {
2798            kind,
2799            read_only,
2800            channel,
2801            device,
2802            is_dvd,
2803        })
2804    }
2805}
2806
2807// <kind>[,ro]
2808#[derive(Clone, Debug, PartialEq, vmm_cli::KeyValueArgs)]
2809pub struct FloppyDiskCli {
2810    #[kv(positional)]
2811    pub kind: DiskCliKind,
2812    #[kv(flag, key = "ro")]
2813    pub read_only: bool,
2814}
2815
2816#[derive(Clone)]
2817pub struct DebugconSerialConfigCli {
2818    pub port: u16,
2819    pub serial: SerialConfigCli,
2820}
2821
2822impl FromStr for DebugconSerialConfigCli {
2823    type Err = String;
2824
2825    fn from_str(s: &str) -> Result<Self, Self::Err> {
2826        let Some((port, serial)) = s.split_once(',') else {
2827            return Err("invalid format (missing comma between port and serial)".into());
2828        };
2829
2830        let port: u16 = parse_number(port)
2831            .map_err(|_| "could not parse port".to_owned())?
2832            .try_into()
2833            .map_err(|_| "port must be 16-bit")?;
2834        let serial: SerialConfigCli = serial.parse()?;
2835
2836        Ok(Self { port, serial })
2837    }
2838}
2839
2840/// A COM port binding, optionally prefixed with `debugger-mode:` to run the
2841/// port in debugger mode for WinDbg / KD-over-serial.
2842#[derive(Clone, Debug, PartialEq)]
2843pub struct ComSerialConfigCli {
2844    /// Whether this COM port runs in debugger mode (for WinDbg / KD-over-serial).
2845    pub debugger_mode: bool,
2846    /// The serial backend for this COM port.
2847    pub backend: SerialConfigCli,
2848}
2849
2850impl FromStr for ComSerialConfigCli {
2851    type Err = String;
2852
2853    fn from_str(s: &str) -> Result<Self, Self::Err> {
2854        match s.strip_prefix("debugger-mode:") {
2855            Some(rest) => Ok(Self {
2856                debugger_mode: true,
2857                backend: rest.parse()?,
2858            }),
2859            None => Ok(Self {
2860                debugger_mode: false,
2861                backend: s.parse()?,
2862            }),
2863        }
2864    }
2865}
2866
2867/// (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>]\[,name=\<windowtitle\>\] | null | none)
2868#[derive(Clone, Debug, PartialEq)]
2869pub enum SerialConfigCli {
2870    None,
2871    Null,
2872    Console,
2873    NewConsole(Option<PathBuf>, Option<String>),
2874    Stderr,
2875    Pipe(PathBuf),
2876    Tcp(SocketAddr),
2877    File(PathBuf),
2878}
2879
2880impl FromStr for SerialConfigCli {
2881    type Err = String;
2882
2883    fn from_str(s: &str) -> Result<Self, Self::Err> {
2884        let keyvalues = SerialConfigCli::parse_keyvalues(s)?;
2885
2886        let first_key = match keyvalues.first() {
2887            Some(first_pair) => first_pair.0.as_str(),
2888            None => Err("invalid serial configuration: no values supplied")?,
2889        };
2890        let first_value = keyvalues.first().unwrap().1.as_ref();
2891
2892        let ret = match first_key {
2893            "none" => SerialConfigCli::None,
2894            "null" => SerialConfigCli::Null,
2895            "console" => SerialConfigCli::Console,
2896            "stderr" => SerialConfigCli::Stderr,
2897            "file" => match first_value {
2898                Some(path) => SerialConfigCli::File(path.into()),
2899                None => Err("invalid serial configuration: file requires a value")?,
2900            },
2901            "term" => {
2902                // If user supplies a name key, use it to title the window
2903                let window_name = keyvalues.iter().find(|(key, _)| key == "name");
2904                let window_name = match window_name {
2905                    Some((_, Some(name))) => Some(name.clone()),
2906                    _ => None,
2907                };
2908
2909                SerialConfigCli::NewConsole(first_value.map(|p| p.into()), window_name)
2910            }
2911            "listen" => match first_value {
2912                Some(path) => {
2913                    if let Some(tcp) = path.strip_prefix("tcp:") {
2914                        let addr = tcp
2915                            .parse()
2916                            .map_err(|err| format!("invalid tcp address: {err}"))?;
2917                        SerialConfigCli::Tcp(addr)
2918                    } else {
2919                        SerialConfigCli::Pipe(path.into())
2920                    }
2921                }
2922                None => Err(
2923                    "invalid serial configuration: listen requires a value of tcp:addr or pipe",
2924                )?,
2925            },
2926            _ => {
2927                return Err(format!(
2928                    "invalid serial configuration: '{}' is not a known option",
2929                    first_key
2930                ));
2931            }
2932        };
2933
2934        Ok(ret)
2935    }
2936}
2937
2938impl SerialConfigCli {
2939    /// Parse a comma separated list of key=value options into a vector of
2940    /// key/value pairs.
2941    fn parse_keyvalues(s: &str) -> Result<Vec<(String, Option<String>)>, String> {
2942        let mut ret = Vec::new();
2943
2944        // For each comma separated item in the supplied list
2945        for item in s.split(',') {
2946            // Split on the = for key and value
2947            // If no = is found, treat key as key and value as None
2948            let mut eqsplit = item.split('=');
2949            let key = eqsplit.next();
2950            let value = eqsplit.next();
2951
2952            if let Some(key) = key {
2953                ret.push((key.to_owned(), value.map(|x| x.to_owned())));
2954            } else {
2955                // An empty key is invalid
2956                return Err("invalid key=value pair in serial config".into());
2957            }
2958        }
2959        Ok(ret)
2960    }
2961}
2962
2963#[derive(Clone, Debug, PartialEq)]
2964pub enum EndpointConfigCli {
2965    None,
2966    Consomme {
2967        cidr: Option<String>,
2968        host_fwd: Vec<HostPortConfigCli>,
2969    },
2970    Dio {
2971        id: Option<String>,
2972    },
2973    Tap {
2974        name: String,
2975    },
2976}
2977
2978/// Parsed host port forwarding configuration from the CLI.
2979#[derive(Clone, Debug, PartialEq)]
2980pub struct HostPortConfigCli {
2981    pub protocol: HostPortProtocolCli,
2982    pub host_address: Option<std::net::IpAddr>,
2983    pub host_port: u16,
2984    pub guest_port: u16,
2985}
2986
2987/// Protocol for host port forwarding.
2988#[derive(Clone, Debug, PartialEq)]
2989pub enum HostPortProtocolCli {
2990    Tcp,
2991    Udp,
2992}
2993
2994fn parse_hostfwd(s: &str) -> Result<HostPortConfigCli, String> {
2995    // Format: protocol:[hostaddr]:hostport-[guestaddr]:guestport
2996    // Examples: "tcp::3389-:3389", "tcp:127.0.0.1:8080-:80", "tcp:[::1]:8080-:80"
2997    let (host_part, guest_part) = s.split_once('-').ok_or_else(|| {
2998        format!(
2999            "invalid hostfwd format '{s}', \
3000             expected 'proto:[hostaddr]:hostport-[guestaddr]:guestport'"
3001        )
3002    })?;
3003
3004    // Extract protocol from host part (first colon-delimited field)
3005    let (proto, host_addr_port) = host_part.split_once(':').ok_or_else(|| {
3006        format!("invalid hostfwd host part '{host_part}', expected 'proto:[hostaddr]:hostport'")
3007    })?;
3008    let protocol = match proto {
3009        "tcp" => HostPortProtocolCli::Tcp,
3010        "udp" => HostPortProtocolCli::Udp,
3011        other => {
3012            return Err(format!(
3013                "unknown hostfwd protocol '{other}', expected 'tcp' or 'udp'"
3014            ));
3015        }
3016    };
3017
3018    let (host_address, host_port) = parse_addr_port(host_addr_port)
3019        .map_err(|e| format!("invalid hostfwd host address/port: {e}"))?;
3020    let (_, guest_port) = parse_addr_port(guest_part)
3021        .map_err(|e| format!("invalid hostfwd guest address/port: {e}"))?;
3022
3023    Ok(HostPortConfigCli {
3024        protocol,
3025        host_address,
3026        host_port,
3027        guest_port,
3028    })
3029}
3030
3031/// Parse an address-port pair in one of these forms:
3032/// - `[ipv6addr]:port`
3033/// - `addr:port`
3034/// - `:port`  (empty address)
3035/// - `port`   (no address)
3036fn parse_addr_port(s: &str) -> Result<(Option<std::net::IpAddr>, u16), String> {
3037    if let Some(rest) = s.strip_prefix('[') {
3038        // Bracketed IPv6 address: [addr]:port
3039        let (addr, port) = rest
3040            .split_once("]:")
3041            .ok_or_else(|| format!("expected '[addr]:port', got '[{rest}'"))?;
3042        let port: u16 = port.parse().map_err(|_| format!("invalid port '{port}'"))?;
3043        let addr: std::net::IpAddr = addr
3044            .parse()
3045            .map_err(|e| format!("invalid address '{addr}': {e}"))?;
3046        Ok((Some(addr), port))
3047    } else {
3048        match s.rsplit_once(':') {
3049            Some((addr, port)) => {
3050                let port: u16 = port.parse().map_err(|_| format!("invalid port '{port}'"))?;
3051                let addr = if addr.is_empty() {
3052                    None
3053                } else {
3054                    let parsed: std::net::IpAddr = addr
3055                        .parse()
3056                        .map_err(|e| format!("invalid address '{addr}': {e}"))?;
3057                    Some(parsed)
3058                };
3059                Ok((addr, port))
3060            }
3061            None => {
3062                let port: u16 = s.parse().map_err(|_| format!("invalid port '{s}'"))?;
3063                Ok((None, port))
3064            }
3065        }
3066    }
3067}
3068
3069impl FromStr for EndpointConfigCli {
3070    type Err = String;
3071
3072    fn from_str(s: &str) -> Result<Self, Self::Err> {
3073        let ret = match s.split(':').collect::<Vec<_>>().as_slice() {
3074            ["none"] => EndpointConfigCli::None,
3075            ["consomme", rest @ ..] => {
3076                let remaining = rest.join(":");
3077                let mut cidr = None;
3078                let mut host_fwd = Vec::new();
3079                for opt in remaining.split(',').filter(|s| !s.is_empty()) {
3080                    if let Some(fwd) = opt.strip_prefix("hostfwd=") {
3081                        host_fwd.push(parse_hostfwd(fwd)?);
3082                    } else if cidr.is_none() {
3083                        cidr = Some(opt.to_owned());
3084                    } else {
3085                        return Err(format!("unexpected consomme option '{opt}'"));
3086                    }
3087                }
3088                EndpointConfigCli::Consomme { cidr, host_fwd }
3089            }
3090            ["dio", s @ ..] => EndpointConfigCli::Dio {
3091                id: s.first().map(|s| (*s).to_owned()),
3092            },
3093            ["tap", name] => EndpointConfigCli::Tap {
3094                name: (*name).to_owned(),
3095            },
3096            _ => return Err("invalid network backend".into()),
3097        };
3098
3099        Ok(ret)
3100    }
3101}
3102
3103#[derive(Clone, Debug, PartialEq)]
3104pub struct NicConfigCli {
3105    pub vtl: DeviceVtl,
3106    pub endpoint: EndpointConfigCli,
3107    pub max_queues: Option<u16>,
3108    pub underhill: bool,
3109    pub pcie_port: Option<String>,
3110}
3111
3112impl FromStr for NicConfigCli {
3113    type Err = String;
3114
3115    fn from_str(mut s: &str) -> Result<Self, Self::Err> {
3116        let mut vtl = DeviceVtl::Vtl0;
3117        let mut max_queues = None;
3118        let mut underhill = false;
3119        let mut pcie_port = None;
3120        while let Some((opt, rest)) = s.split_once(':') {
3121            if let Some((opt, val)) = opt.split_once('=') {
3122                match opt {
3123                    "queues" => {
3124                        max_queues = Some(val.parse().map_err(|_| "failed to parse queue count")?);
3125                    }
3126                    "pcie_port" => {
3127                        if val.is_empty() {
3128                            return Err("`pcie_port=` requires port name argument".into());
3129                        }
3130                        pcie_port = Some(val.to_string());
3131                    }
3132                    _ => break,
3133                }
3134            } else {
3135                match opt {
3136                    "vtl2" => {
3137                        vtl = DeviceVtl::Vtl2;
3138                    }
3139                    "uh" => underhill = true,
3140                    _ => break,
3141                }
3142            }
3143            s = rest;
3144        }
3145
3146        if underhill && vtl != DeviceVtl::Vtl0 {
3147            return Err("`uh` is incompatible with `vtl2`".into());
3148        }
3149
3150        if pcie_port.is_some() && (underhill || vtl != DeviceVtl::Vtl0) {
3151            return Err("`pcie_port` is incompatible with `uh` and `vtl2`".into());
3152        }
3153
3154        let endpoint = s.parse()?;
3155        Ok(NicConfigCli {
3156            vtl,
3157            endpoint,
3158            max_queues,
3159            underhill,
3160            pcie_port,
3161        })
3162    }
3163}
3164
3165#[derive(Debug, Error)]
3166#[error("unknown VTL2 relocation type: {0}")]
3167pub struct UnknownVtl2RelocationType(String);
3168
3169fn parse_vtl2_relocation(s: &str) -> Result<Vtl2BaseAddressType, UnknownVtl2RelocationType> {
3170    match s {
3171        "disable" => Ok(Vtl2BaseAddressType::File),
3172        s if s.starts_with("auto=") => {
3173            let s = s.strip_prefix("auto=").unwrap_or_default();
3174            let size = if s == "filesize" {
3175                None
3176            } else {
3177                let size = parse_memory(s).map_err(|e| {
3178                    UnknownVtl2RelocationType(format!(
3179                        "unable to parse memory size from {} for 'auto=' type, {e}",
3180                        e
3181                    ))
3182                })?;
3183                Some(size)
3184            };
3185            Ok(Vtl2BaseAddressType::MemoryLayout { size })
3186        }
3187        s if s.starts_with("absolute=") => {
3188            let s = s.strip_prefix("absolute=");
3189            let addr = parse_number(s.unwrap_or_default()).map_err(|e| {
3190                UnknownVtl2RelocationType(format!(
3191                    "unable to parse number from {} for 'absolute=' type",
3192                    e
3193                ))
3194            })?;
3195            Ok(Vtl2BaseAddressType::Absolute(addr))
3196        }
3197        s if s.starts_with("vtl2=") => {
3198            let s = s.strip_prefix("vtl2=").unwrap_or_default();
3199            let size = if s == "filesize" {
3200                None
3201            } else {
3202                let size = parse_memory(s).map_err(|e| {
3203                    UnknownVtl2RelocationType(format!(
3204                        "unable to parse memory size from {} for 'vtl2=' type, {e}",
3205                        e
3206                    ))
3207                })?;
3208                Some(size)
3209            };
3210            Ok(Vtl2BaseAddressType::Vtl2Allocate { size })
3211        }
3212        _ => Err(UnknownVtl2RelocationType(s.to_owned())),
3213    }
3214}
3215
3216#[derive(Debug, Copy, Clone, PartialEq)]
3217pub enum SmtConfigCli {
3218    Auto,
3219    Force,
3220    Off,
3221}
3222
3223#[derive(Debug, Error)]
3224#[error("expected auto, force, or off")]
3225pub struct BadSmtConfig;
3226
3227impl FromStr for SmtConfigCli {
3228    type Err = BadSmtConfig;
3229
3230    fn from_str(s: &str) -> Result<Self, Self::Err> {
3231        let r = match s {
3232            "auto" => Self::Auto,
3233            "force" => Self::Force,
3234            "off" => Self::Off,
3235            _ => return Err(BadSmtConfig),
3236        };
3237        Ok(r)
3238    }
3239}
3240
3241#[cfg_attr(not(guest_arch = "x86_64"), expect(dead_code))]
3242fn parse_x2apic(s: &str) -> Result<X2ApicConfig, &'static str> {
3243    let r = match s {
3244        "auto" => X2ApicConfig::Auto,
3245        "supported" => X2ApicConfig::Supported,
3246        "off" => X2ApicConfig::Unsupported,
3247        "on" => X2ApicConfig::Enabled,
3248        _ => return Err("expected auto, supported, off, or on"),
3249    };
3250    Ok(r)
3251}
3252
3253#[derive(Debug, Copy, Clone, ValueEnum)]
3254pub enum Vtl0LateMapPolicyCli {
3255    Off,
3256    Log,
3257    Halt,
3258    Exception,
3259}
3260
3261/// PCIe MSI controller selection for aarch64.
3262#[derive(Debug, Copy, Clone, Default, ValueEnum)]
3263pub enum GicMsiCli {
3264    /// Use ITS when available, fall back to GICv2m.
3265    #[default]
3266    Auto,
3267    /// Force GICv3 ITS (LPI-based MSIs).
3268    Its,
3269    /// Force GICv2m (SPI-based MSIs).
3270    V2m,
3271}
3272
3273#[derive(Debug, Copy, Clone, ValueEnum)]
3274pub enum IsolationCli {
3275    Vbs,
3276    Snp,
3277}
3278
3279#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)]
3280pub enum IgvmPersonalityCli {
3281    /// OpenHCL paravisor hosted in VTL2.
3282    Openhcl,
3283    /// UEFI firmware loaded directly from the IGVM.
3284    Uefi,
3285    /// Linux kernel loaded directly from the IGVM.
3286    LinuxDirect,
3287}
3288
3289impl FromStr for IgvmPersonalityCli {
3290    type Err = String;
3291
3292    fn from_str(value: &str) -> Result<Self, Self::Err> {
3293        ValueEnum::from_str(value, false)
3294    }
3295}
3296
3297#[derive(Debug, Copy, Clone, PartialEq)]
3298pub struct PcatBootOrderCli(pub [PcatBootDevice; 4]);
3299
3300impl FromStr for PcatBootOrderCli {
3301    type Err = &'static str;
3302
3303    fn from_str(s: &str) -> Result<Self, Self::Err> {
3304        let mut default_order = DEFAULT_PCAT_BOOT_ORDER.map(Some);
3305        let mut order = Vec::new();
3306
3307        for item in s.split(',') {
3308            let device = match item {
3309                "optical" => PcatBootDevice::Optical,
3310                "hdd" => PcatBootDevice::HardDrive,
3311                "net" => PcatBootDevice::Network,
3312                "floppy" => PcatBootDevice::Floppy,
3313                _ => return Err("unknown boot device type"),
3314            };
3315
3316            let default_pos = default_order
3317                .iter()
3318                .position(|x| x == &Some(device))
3319                .ok_or("cannot pass duplicate boot devices")?;
3320
3321            order.push(default_order[default_pos].take().unwrap());
3322        }
3323
3324        order.extend(default_order.into_iter().flatten());
3325        assert_eq!(order.len(), 4);
3326
3327        Ok(Self(order.try_into().unwrap()))
3328    }
3329}
3330
3331#[derive(Copy, Clone, Debug, ValueEnum)]
3332pub enum UefiConsoleModeCli {
3333    Default,
3334    Com1,
3335    Com2,
3336    None,
3337}
3338
3339impl FromStr for UefiConsoleModeCli {
3340    type Err = String;
3341
3342    fn from_str(value: &str) -> Result<Self, Self::Err> {
3343        ValueEnum::from_str(value, false)
3344    }
3345}
3346
3347#[derive(Copy, Clone, Debug, Default, ValueEnum)]
3348pub enum EfiDiagnosticsLogLevelCli {
3349    #[default]
3350    Default,
3351    Info,
3352    Full,
3353}
3354
3355impl FromStr for EfiDiagnosticsLogLevelCli {
3356    type Err = String;
3357
3358    fn from_str(value: &str) -> Result<Self, Self::Err> {
3359        ValueEnum::from_str(value, false)
3360    }
3361}
3362
3363#[derive(Clone, Debug, PartialEq)]
3364pub struct PcieRootComplexCli {
3365    pub name: String,
3366    pub segment: u16,
3367    pub start_bus: u8,
3368    pub end_bus: u8,
3369    pub low_mmio: u32,
3370    pub high_mmio: u64,
3371    pub low_mmio_base: Option<u64>,
3372    pub high_mmio_base: Option<u64>,
3373    pub preserve_bars: bool,
3374    pub hdm: u64,
3375    pub hdm_window_restrictions: CfmwsWindowRestrictions,
3376    pub vnode: Option<u32>,
3377}
3378
3379/// A `0x`-prefixed hexadecimal address, used for MMIO base overrides.
3380struct HexAddress(u64);
3381
3382impl FromStr for HexAddress {
3383    type Err = anyhow::Error;
3384
3385    fn from_str(s: &str) -> anyhow::Result<Self> {
3386        Ok(HexAddress(parse_address(s)?))
3387    }
3388}
3389
3390/// A CFMWS window-restrictions bitmask (`0x21` or `33`).
3391struct CfmwsWindowRestrictionsCli(CfmwsWindowRestrictions);
3392
3393impl FromStr for CfmwsWindowRestrictionsCli {
3394    type Err = anyhow::Error;
3395
3396    fn from_str(s: &str) -> anyhow::Result<Self> {
3397        Ok(CfmwsWindowRestrictionsCli(
3398            parse_cxl_cfmws_window_restriction_u16_bitmask(s)?,
3399        ))
3400    }
3401}
3402
3403/// Raw `--pcie-root-complex` options, parsed declaratively. Validated and
3404/// mapped into the public [`PcieRootComplexCli`] by its `FromStr`.
3405#[derive(vmm_cli::KeyValueArgs)]
3406struct PcieRootComplexArgs {
3407    #[kv(positional)]
3408    name: String,
3409    #[kv(default)]
3410    segment: u16,
3411    #[kv(default)]
3412    start_bus: u8,
3413    #[kv(default = 255)]
3414    end_bus: u8,
3415    #[kv(default = vmm_cli::MemorySize(64 * 1024 * 1024))]
3416    low_mmio: vmm_cli::MemorySize,
3417    #[kv(default = vmm_cli::MemorySize(1024 * 1024 * 1024))]
3418    high_mmio: vmm_cli::MemorySize,
3419    low_mmio_base: Option<HexAddress>,
3420    high_mmio_base: Option<HexAddress>,
3421    #[kv(flag)]
3422    preserve_bars: bool,
3423    #[kv(default = vmm_cli::MemorySize(1024 * 1024 * 1024))]
3424    hdm: vmm_cli::MemorySize,
3425    #[kv(default = CfmwsWindowRestrictionsCli(CfmwsWindowRestrictions::DEVICE_COHERENT))]
3426    hdm_window_restrictions: CfmwsWindowRestrictionsCli,
3427    #[kv(key = "node")]
3428    vnode: Option<u32>,
3429}
3430
3431impl FromStr for PcieRootComplexCli {
3432    type Err = anyhow::Error;
3433
3434    fn from_str(s: &str) -> Result<Self, Self::Err> {
3435        let args: PcieRootComplexArgs = s.parse()?;
3436
3437        if args.start_bus > args.end_bus {
3438            anyhow::bail!("start_bus must be <= end_bus");
3439        }
3440
3441        let low_mmio = u32::try_from(args.low_mmio.0).context("low MMIO size exceeds 32 bits")?;
3442
3443        Ok(PcieRootComplexCli {
3444            name: args.name,
3445            segment: args.segment,
3446            start_bus: args.start_bus,
3447            end_bus: args.end_bus,
3448            low_mmio,
3449            high_mmio: args.high_mmio.0,
3450            low_mmio_base: args.low_mmio_base.map(|a| a.0),
3451            high_mmio_base: args.high_mmio_base.map(|a| a.0),
3452            preserve_bars: args.preserve_bars,
3453            hdm: args.hdm.0,
3454            hdm_window_restrictions: args.hdm_window_restrictions.0,
3455            vnode: args.vnode,
3456        })
3457    }
3458}
3459
3460fn parse_cxl_cfmws_window_restriction_u16_bitmask(
3461    s: &str,
3462) -> anyhow::Result<CfmwsWindowRestrictions> {
3463    let bits = if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
3464        u16::from_str_radix(hex, 16).context("invalid hex bitmask")?
3465    } else {
3466        u16::from_str(s).context("invalid decimal bitmask")?
3467    };
3468
3469    CfmwsWindowRestrictions::try_from_bits(bits)
3470        .context("bitmask includes reserved CFMWS window restriction bits")
3471}
3472
3473#[derive(Clone, Debug, PartialEq)]
3474pub struct PcieRootPortCli {
3475    pub root_complex_name: String,
3476    pub name: String,
3477    pub devfn: Option<u8>,
3478    pub hotplug: bool,
3479    pub acs_capabilities_supported: Option<u16>,
3480    pub cxl: bool,
3481    pub pasid: bool,
3482}
3483
3484/// A colon-joined `parent:child` name pair used as the positional head of
3485/// `--pcie-root-port` and `--pcie-switch`.
3486struct PortNamePair {
3487    parent: String,
3488    child: String,
3489}
3490
3491impl FromStr for PortNamePair {
3492    type Err = anyhow::Error;
3493
3494    fn from_str(s: &str) -> anyhow::Result<Self> {
3495        let mut it = s.split(':');
3496        let parent = it
3497            .next()
3498            .filter(|x| !x.is_empty())
3499            .context("expected parent name")?;
3500        let child = it
3501            .next()
3502            .filter(|x| !x.is_empty())
3503            .context("expected child name")?;
3504        anyhow::ensure!(it.next().is_none(), "unexpected token in '{s}'");
3505        Ok(PortNamePair {
3506            parent: parent.to_string(),
3507            child: child.to_string(),
3508        })
3509    }
3510}
3511
3512/// A PCIe device/function address (`XX[.Y]`) parsed into a devfn.
3513struct PcieAddr(u8);
3514
3515impl FromStr for PcieAddr {
3516    type Err = anyhow::Error;
3517
3518    fn from_str(s: &str) -> anyhow::Result<Self> {
3519        Ok(PcieAddr(parse_pcie_addr(s)?))
3520    }
3521}
3522
3523/// An ACS capability bitmask (hex `0x..` or decimal).
3524struct AcsMask(u16);
3525
3526impl FromStr for AcsMask {
3527    type Err = anyhow::Error;
3528
3529    fn from_str(s: &str) -> anyhow::Result<Self> {
3530        Ok(AcsMask(parse_acs_capability_mask(s)?))
3531    }
3532}
3533
3534/// Raw `--pcie-root-port` options, mapped into [`PcieRootPortCli`].
3535#[derive(vmm_cli::KeyValueArgs)]
3536struct RootPortArgs {
3537    #[kv(positional)]
3538    names: PortNamePair,
3539    addr: Option<PcieAddr>,
3540    #[kv(flag)]
3541    hotplug: bool,
3542    acs: Option<AcsMask>,
3543    #[kv(flag)]
3544    cxl: bool,
3545    #[kv(flag)]
3546    pasid: bool,
3547}
3548
3549impl FromStr for PcieRootPortCli {
3550    type Err = anyhow::Error;
3551
3552    fn from_str(s: &str) -> Result<Self, Self::Err> {
3553        let args: RootPortArgs = s.parse()?;
3554        Ok(PcieRootPortCli {
3555            root_complex_name: args.names.parent,
3556            name: args.names.child,
3557            devfn: args.addr.map(|a| a.0),
3558            hotplug: args.hotplug,
3559            acs_capabilities_supported: args.acs.map(|a| a.0),
3560            cxl: args.cxl,
3561            pasid: args.pasid,
3562        })
3563    }
3564}
3565
3566/// Parses a PCIe address of the form `XX[.Y]`, where `XX` is the device number
3567/// (0-31) and the optional `Y` is the function number (0-7), into a devfn
3568/// (`device << 3 | function`).
3569fn parse_pcie_addr(s: &str) -> anyhow::Result<u8> {
3570    let parse_int = |v: &str| -> anyhow::Result<u8> {
3571        if let Some(hex) = v.strip_prefix("0x").or_else(|| v.strip_prefix("0X")) {
3572            u8::from_str_radix(hex, 16).context("invalid hex number")
3573        } else {
3574            v.parse().context("invalid number")
3575        }
3576    };
3577
3578    let mut parts = s.split('.');
3579    let device = parse_int(parts.next().context("expected device number")?)?;
3580    let function = match parts.next() {
3581        Some(f) => parse_int(f)?,
3582        None => 0,
3583    };
3584    if parts.next().is_some() {
3585        anyhow::bail!("unexpected token in addr '{s}'");
3586    }
3587    if device > 31 {
3588        anyhow::bail!("device number {device} out of range (0-31)");
3589    }
3590    if function > 7 {
3591        anyhow::bail!("function number {function} out of range (0-7)");
3592    }
3593    Ok((device << 3) | function)
3594}
3595
3596#[derive(Clone, Debug, PartialEq)]
3597pub struct GenericPcieSwitchCli {
3598    pub port_name: String,
3599    pub name: String,
3600    pub num_downstream_ports: u8,
3601    pub hotplug: bool,
3602    pub acs_capabilities_supported: Option<u16>,
3603    pub pasid: bool,
3604}
3605
3606/// Raw `--pcie-switch` options, mapped into [`GenericPcieSwitchCli`].
3607#[derive(vmm_cli::KeyValueArgs)]
3608struct SwitchArgs {
3609    #[kv(positional)]
3610    names: PortNamePair,
3611    #[kv(default = 4)]
3612    num_downstream_ports: u8,
3613    #[kv(flag)]
3614    hotplug: bool,
3615    acs: Option<AcsMask>,
3616    #[kv(flag)]
3617    pasid: bool,
3618}
3619
3620impl FromStr for GenericPcieSwitchCli {
3621    type Err = anyhow::Error;
3622
3623    fn from_str(s: &str) -> Result<Self, Self::Err> {
3624        let args: SwitchArgs = s.parse()?;
3625        Ok(GenericPcieSwitchCli {
3626            port_name: args.names.parent,
3627            name: args.names.child,
3628            num_downstream_ports: args.num_downstream_ports,
3629            hotplug: args.hotplug,
3630            acs_capabilities_supported: args.acs.map(|a| a.0),
3631            pasid: args.pasid,
3632        })
3633    }
3634}
3635
3636/// CLI configuration mapping a PCIe port name to a generic-initiator NUMA node.
3637#[derive(Clone, Debug, PartialEq, vmm_cli::KeyValueArgs)]
3638pub struct PcieGenericInitiatorCli {
3639    /// Name of the PCIe port (root port or switch downstream port) behind
3640    /// which the generic-initiator device resides.
3641    #[kv(key = "port")]
3642    pub port_name: String,
3643    /// NUMA node the device is a generic initiator for.
3644    pub node: u32,
3645}
3646
3647/// CLI configuration for a PCIe remote device.
3648#[derive(Clone, Debug, PartialEq, vmm_cli::KeyValueArgs)]
3649pub struct PcieRemoteCli {
3650    /// Name of the PCIe downstream port to attach to.
3651    #[kv(positional)]
3652    pub port_name: String,
3653    /// TCP socket address for the remote simulator.
3654    #[kv(key = "socket")]
3655    pub socket_addr: Option<String>,
3656    /// Hardware unit identifier for plug request.
3657    #[kv(default)]
3658    pub hu: u16,
3659    /// Controller identifier for plug request.
3660    #[kv(default)]
3661    pub controller: u16,
3662}
3663
3664/// CLI configuration for a VFIO-assigned PCI device.
3665///
3666/// Syntax: `host=<bdf>,port=<name>[,iommu=<id>][,barN=host|barN=0x<addr>]`
3667#[cfg(target_os = "linux")]
3668#[derive(Clone, Debug)]
3669pub struct VfioDeviceCli {
3670    /// Name of the PCIe downstream port to attach to.
3671    pub port_name: String,
3672    /// PCI BDF address of the device on the host (e.g., "0000:01:00.0").
3673    pub pci_id: String,
3674    /// Optional iommufd context ID. When set, uses VFIO cdev + iommufd
3675    /// instead of the legacy group/container path.
3676    pub iommu: Option<String>,
3677    /// Per-BAR pre-programming configuration.
3678    pub bar_addresses: [vfio_assigned_device_resources::BarAddressConfig; 6],
3679}
3680
3681/// Per-BAR address configuration parsed from the CLI.
3682#[cfg(target_os = "linux")]
3683struct BarAddressCli(vfio_assigned_device_resources::BarAddressConfig);
3684
3685#[cfg(target_os = "linux")]
3686impl FromStr for BarAddressCli {
3687    type Err = anyhow::Error;
3688
3689    fn from_str(s: &str) -> anyhow::Result<Self> {
3690        let config = if s == "host" {
3691            vfio_assigned_device_resources::BarAddressConfig::HostAssigned
3692        } else if let Some(value) = s.strip_prefix("0x") {
3693            let address = u64::from_str_radix(value, 16).context("invalid BAR address")?;
3694            anyhow::ensure!(address != 0, "BAR address must be nonzero");
3695            vfio_assigned_device_resources::BarAddressConfig::Fixed(address)
3696        } else {
3697            anyhow::bail!("expected 'host' or a hexadecimal address starting with '0x'");
3698        };
3699        Ok(Self(config))
3700    }
3701}
3702
3703/// Per-BAR address configuration, flattened into
3704/// [`VfioArgs`].
3705#[cfg(target_os = "linux")]
3706#[derive(vmm_cli::KeyValueArgs)]
3707struct BarFlags {
3708    bar0: Option<BarAddressCli>,
3709    bar1: Option<BarAddressCli>,
3710    bar2: Option<BarAddressCli>,
3711    bar3: Option<BarAddressCli>,
3712    bar4: Option<BarAddressCli>,
3713    bar5: Option<BarAddressCli>,
3714}
3715
3716/// Raw `--vfio` options, resolved and validated into a [`VfioDeviceCli`].
3717#[cfg(target_os = "linux")]
3718#[derive(vmm_cli::KeyValueArgs)]
3719struct VfioArgs {
3720    host: String,
3721    port: String,
3722    iommu: Option<String>,
3723    #[kv(flatten)]
3724    bars: BarFlags,
3725}
3726
3727#[cfg(target_os = "linux")]
3728impl FromStr for VfioDeviceCli {
3729    type Err = anyhow::Error;
3730
3731    fn from_str(s: &str) -> Result<Self, Self::Err> {
3732        let args: VfioArgs = s.parse()?;
3733
3734        // Reject path separators to prevent sysfs path traversal via Path::join.
3735        if args.host.contains('/') || args.host.contains("..") {
3736            anyhow::bail!("PCI address must not contain path separators");
3737        }
3738
3739        let bars = args.bars;
3740        let bar_addresses = [
3741            bars.bar0.map(|bar| bar.0).unwrap_or_default(),
3742            bars.bar1.map(|bar| bar.0).unwrap_or_default(),
3743            bars.bar2.map(|bar| bar.0).unwrap_or_default(),
3744            bars.bar3.map(|bar| bar.0).unwrap_or_default(),
3745            bars.bar4.map(|bar| bar.0).unwrap_or_default(),
3746            bars.bar5.map(|bar| bar.0).unwrap_or_default(),
3747        ];
3748
3749        Ok(VfioDeviceCli {
3750            port_name: args.port,
3751            pci_id: args.host,
3752            iommu: args.iommu,
3753            bar_addresses,
3754        })
3755    }
3756}
3757
3758/// CLI configuration for an SMMUv3 instance.
3759///
3760/// Syntax: `rc=<name>[,accel][,oas=auto|N]`. `oas` defaults to `auto`.
3761#[cfg(guest_arch = "aarch64")]
3762#[derive(Clone, Debug, vmm_cli::KeyValueArgs)]
3763pub struct SmmuCli {
3764    /// Name of the PCIe root complex this SMMU covers.
3765    #[kv(key = "rc")]
3766    pub rc_name: String,
3767    /// Enable HW-accelerated nested translation (iommufd).
3768    #[kv(flag)]
3769    pub accel: bool,
3770    /// Output address size policy.
3771    #[kv(default)]
3772    pub oas: SmmuOasCli,
3773}
3774
3775/// Output address size (OAS) policy parsed from `--smmu`.
3776#[cfg(guest_arch = "aarch64")]
3777#[derive(Clone, Copy, Debug, Default)]
3778pub enum SmmuOasCli {
3779    /// Advertise a fixed default OAS (see the `--smmu` docs for the sizing
3780    /// policy and when a larger fixed OAS is required).
3781    #[default]
3782    Auto,
3783    /// Fixed OAS in bits (one of 32, 36, 40, 42, 44, 48, 52).
3784    Fixed(u8),
3785}
3786
3787#[cfg(guest_arch = "aarch64")]
3788impl FromStr for SmmuOasCli {
3789    type Err = anyhow::Error;
3790
3791    fn from_str(s: &str) -> Result<Self, Self::Err> {
3792        Ok(if s == "auto" {
3793            SmmuOasCli::Auto
3794        } else {
3795            SmmuOasCli::Fixed(s.parse().context("oas must be 'auto' or a number")?)
3796        })
3797    }
3798}
3799
3800/// CLI configuration for an iommufd context.
3801///
3802/// Syntax: `id=<name>`
3803#[cfg(target_os = "linux")]
3804#[derive(Clone, Debug, vmm_cli::KeyValueArgs)]
3805pub struct IommuCli {
3806    /// Unique identifier for this iommufd context.
3807    pub id: String,
3808}
3809
3810/// Read a environment variable that may / may-not have a target-specific
3811/// prefix. e.g: `default_value_from_arch_env("FOO")` would first try and read
3812/// from `FOO`, and if that's not found, it will try `X86_64_FOO`.
3813///
3814/// Must return an `OsString`, in order to be compatible with `clap`'s
3815/// default_value code. As such - to encode the absence of the env-var, an empty
3816/// OsString is returned.
3817fn default_value_from_arch_env(name: &str) -> OsString {
3818    let prefix = if cfg!(guest_arch = "x86_64") {
3819        "X86_64"
3820    } else if cfg!(guest_arch = "aarch64") {
3821        "AARCH64"
3822    } else {
3823        return Default::default();
3824    };
3825    let prefixed = format!("{}_{}", prefix, name);
3826    std::env::var_os(name)
3827        .or_else(|| std::env::var_os(prefixed))
3828        .unwrap_or_default()
3829}
3830
3831pub fn default_uefi_firmware() -> Option<PathBuf> {
3832    OptionalPathBuf::from(default_value_from_arch_env("OPENVMM_UEFI_FIRMWARE").as_os_str()).0
3833}
3834
3835/// Workaround to use `Option<PathBuf>` alongside [`default_value_from_arch_env`]
3836#[derive(Clone)]
3837pub struct OptionalPathBuf(pub Option<PathBuf>);
3838
3839impl From<&std::ffi::OsStr> for OptionalPathBuf {
3840    fn from(s: &std::ffi::OsStr) -> Self {
3841        OptionalPathBuf(if s.is_empty() { None } else { Some(s.into()) })
3842    }
3843}
3844
3845#[cfg(target_os = "linux")]
3846#[derive(Clone)]
3847pub enum VhostUserDeviceTypeCli {
3848    /// Block device — config from backend via GET_CONFIG, with num_queues
3849    /// patched by the frontend.
3850    Blk {
3851        num_queues: Option<u16>,
3852        queue_size: Option<u16>,
3853    },
3854    /// Filesystem device — frontend-owned config with mount tag.
3855    Fs {
3856        tag: String,
3857        num_queues: Option<u16>,
3858        queue_size: Option<u16>,
3859    },
3860    /// Generic device identified by numeric virtio device ID.
3861    Other {
3862        device_id: u16,
3863        queue_sizes: Vec<u16>,
3864    },
3865}
3866
3867#[cfg(target_os = "linux")]
3868#[derive(Clone)]
3869pub struct VhostUserCli {
3870    pub socket_path: String,
3871    pub device_type: VhostUserDeviceTypeCli,
3872    pub pcie_port: Option<String>,
3873}
3874
3875/// Raw `--vhost-user` options, resolved into a [`VhostUserCli`] by its
3876/// `FromStr`.
3877#[cfg(target_os = "linux")]
3878#[derive(vmm_cli::KeyValueArgs)]
3879struct VhostUserArgs {
3880    #[kv(positional)]
3881    socket_path: String,
3882    #[kv(key = "type")]
3883    type_name: Option<String>,
3884    device_id: Option<u16>,
3885    tag: Option<String>,
3886    pcie_port: Option<String>,
3887    num_queues: Option<u16>,
3888    queue_size: Option<u16>,
3889    queue_sizes: Option<vmm_cli::BracketList<u16>>,
3890}
3891
3892#[cfg(target_os = "linux")]
3893impl FromStr for VhostUserCli {
3894    type Err = anyhow::Error;
3895
3896    fn from_str(s: &str) -> anyhow::Result<Self> {
3897        let mut args: VhostUserArgs = s.parse()?;
3898        let type_name = args.type_name.take();
3899
3900        if type_name.is_some() == args.device_id.is_some() {
3901            anyhow::bail!("must specify type=<name> or device_id=<N>");
3902        }
3903
3904        // Each variant consumes the options it accepts; whatever is left over
3905        // afterward was used with the wrong device type.
3906        let device_type = match type_name.as_deref() {
3907            Some("fs") => VhostUserDeviceTypeCli::Fs {
3908                tag: args.tag.take().context("type=fs requires tag=<name>")?,
3909                num_queues: args.num_queues.take(),
3910                queue_size: args.queue_size.take(),
3911            },
3912            Some("blk") => VhostUserDeviceTypeCli::Blk {
3913                num_queues: args.num_queues.take(),
3914                queue_size: args.queue_size.take(),
3915            },
3916            Some(ty) => anyhow::bail!("unknown vhost-user device type: '{ty}'"),
3917            None => {
3918                let queue_sizes = args
3919                    .queue_sizes
3920                    .take()
3921                    .context("device_id= requires queue_sizes=[N,N,...]")?
3922                    .0;
3923                anyhow::ensure!(!queue_sizes.is_empty(), "queue_sizes must be non-empty");
3924                VhostUserDeviceTypeCli::Other {
3925                    device_id: args.device_id.unwrap(),
3926                    queue_sizes,
3927                }
3928            }
3929        };
3930
3931        if args.tag.is_some() {
3932            anyhow::bail!("tag= is only valid for type=fs");
3933        }
3934        if args.queue_sizes.is_some() {
3935            anyhow::bail!("queue_sizes= is only valid for device_id=");
3936        }
3937        if args.num_queues.is_some() || args.queue_size.is_some() {
3938            anyhow::bail!(
3939                "num_queues= and queue_size= are not valid for device_id=; use queue_sizes="
3940            );
3941        }
3942
3943        Ok(VhostUserCli {
3944            socket_path: args.socket_path,
3945            device_type,
3946            pcie_port: args.pcie_port,
3947        })
3948    }
3949}
3950
3951#[cfg(test)]
3952mod tests {
3953    use super::*;
3954
3955    use std::path::Path;
3956    use test_with_tracing::test;
3957
3958    /// `--version` reports the resolved build identity rather than clap's
3959    /// default, which would be the parser crate's own name and version.
3960    #[test]
3961    fn version_reports_build_info() {
3962        let short = version_output(["openvmm", "-V"]);
3963        assert_eq!(
3964            short,
3965            format!("openvmm {}\n", openvmm_build_info::get().version())
3966        );
3967
3968        let long = version_output(["openvmm", "--version"]);
3969        assert_eq!(
3970            long,
3971            format!("openvmm {}\n", openvmm_build_info::get().long_version())
3972        );
3973    }
3974
3975    fn version_output(args: [&str; 2]) -> String {
3976        let Err(error) = Options::try_parse_from(args) else {
3977            panic!("{args:?} unexpectedly parsed as runtime options");
3978        };
3979        assert_eq!(error.kind(), clap::error::ErrorKind::DisplayVersion);
3980        error.to_string()
3981    }
3982
3983    #[test]
3984    fn test_parse_rpc() {
3985        // explicit path, default transport
3986        let rpc = RpcCli::from_str("path=/tmp/openvmm.sock").unwrap();
3987        assert_eq!(rpc.path, Path::new("/tmp/openvmm.sock"));
3988        assert_eq!(rpc.transport, RpcTransportCli::Auto);
3989
3990        // explicit transport
3991        for (s, transport) in [
3992            ("auto", RpcTransportCli::Auto),
3993            ("ttrpc", RpcTransportCli::Ttrpc),
3994            ("grpc", RpcTransportCli::Grpc),
3995        ] {
3996            let rpc = RpcCli::from_str(&format!("path=/tmp/s.sock,transport={s}")).unwrap();
3997            assert_eq!(rpc.path, Path::new("/tmp/s.sock"));
3998            assert_eq!(rpc.transport, transport);
3999        }
4000
4001        // errors
4002        assert!(RpcCli::from_str("").is_err());
4003        assert!(RpcCli::from_str("transport=ttrpc").is_err());
4004        assert!(RpcCli::from_str("path=").is_err());
4005        assert!(RpcCli::from_str("path=/tmp/s.sock,transport=bogus").is_err());
4006        assert!(RpcCli::from_str("path=/tmp/s.sock,bogus=1").is_err());
4007        assert!(RpcCli::from_str("path=/a,path=/b").is_err());
4008    }
4009
4010    #[test]
4011    fn test_parse_file_opts() {
4012        // file: prefix with create
4013        let disk = DiskCliKind::from_str("file:test.vhd;create=1G").unwrap();
4014        assert!(matches!(
4015            &disk,
4016            DiskCliKind::File { path, create_with_len: Some(len), direct: false }
4017                if path == Path::new("test.vhd") && *len == 1024 * 1024 * 1024
4018        ));
4019
4020        // bare path with create (no file: prefix)
4021        let disk = DiskCliKind::from_str("test.vhd;create=1G").unwrap();
4022        assert!(matches!(
4023            &disk,
4024            DiskCliKind::File { path, create_with_len: Some(len), direct: false }
4025                if path == Path::new("test.vhd") && *len == 1024 * 1024 * 1024
4026        ));
4027
4028        // direct flag
4029        let disk = DiskCliKind::from_str("file:/dev/sdb;direct").unwrap();
4030        assert!(matches!(
4031            &disk,
4032            DiskCliKind::File { path, create_with_len: None, direct: true }
4033                if path == Path::new("/dev/sdb")
4034        ));
4035
4036        // direct + create in either order
4037        let disk = DiskCliKind::from_str("file:disk.img;direct;create=1G").unwrap();
4038        assert!(matches!(
4039            &disk,
4040            DiskCliKind::File { path, create_with_len: Some(len), direct: true }
4041                if path == Path::new("disk.img") && *len == 1024 * 1024 * 1024
4042        ));
4043
4044        let disk = DiskCliKind::from_str("file:disk.img;create=1G;direct").unwrap();
4045        assert!(matches!(
4046            &disk,
4047            DiskCliKind::File { path, create_with_len: Some(len), direct: true }
4048                if path == Path::new("disk.img") && *len == 1024 * 1024 * 1024
4049        ));
4050
4051        // plain path, no options
4052        let disk = DiskCliKind::from_str("file:disk.img").unwrap();
4053        assert!(matches!(
4054            &disk,
4055            DiskCliKind::File { path, create_with_len: None, direct: false }
4056                if path == Path::new("disk.img")
4057        ));
4058
4059        // invalid option rejected
4060        assert!(DiskCliKind::from_str("file:disk.img;bogus").is_err());
4061
4062        // direct rejected for sql disks
4063        assert!(DiskCliKind::from_str("sql:db.sqlite;direct").is_err());
4064    }
4065
4066    #[test]
4067    fn test_parse_memory_disk() {
4068        let s = "mem:1G";
4069        let disk = DiskCliKind::from_str(s).unwrap();
4070        match disk {
4071            DiskCliKind::Memory(size) => {
4072                assert_eq!(size, 1024 * 1024 * 1024); // 1G
4073            }
4074            _ => panic!("Expected Memory variant"),
4075        }
4076    }
4077
4078    #[test]
4079    fn test_parse_pcie_disk() {
4080        assert_eq!(
4081            DiskCli::from_str("mem:1G,pcie_port=p0").unwrap().pcie_port,
4082            Some("p0".to_string())
4083        );
4084        assert_eq!(
4085            DiskCli::from_str("file:path.vhdx,pcie_port=p0")
4086                .unwrap()
4087                .pcie_port,
4088            Some("p0".to_string())
4089        );
4090        assert_eq!(
4091            DiskCli::from_str("memdiff:file:path.vhdx,pcie_port=p0")
4092                .unwrap()
4093                .pcie_port,
4094            Some("p0".to_string())
4095        );
4096
4097        // Missing port name
4098        assert!(DiskCli::from_str("file:disk.vhd,pcie_port=").is_err());
4099
4100        // Incompatible with various other disk fields
4101        assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,vtl2").is_err());
4102        assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,uh").is_err());
4103        assert!(DiskCli::from_str("file:disk.vhd,pcie_port=p0,uh-nvme").is_err());
4104    }
4105
4106    #[test]
4107    fn test_parse_memory_diff_disk() {
4108        let s = "memdiff:file:base.img";
4109        let disk = DiskCliKind::from_str(s).unwrap();
4110        match disk {
4111            DiskCliKind::MemoryDiff(inner) => match *inner {
4112                DiskCliKind::File {
4113                    path,
4114                    create_with_len,
4115                    ..
4116                } => {
4117                    assert_eq!(path, PathBuf::from("base.img"));
4118                    assert_eq!(create_with_len, None);
4119                }
4120                _ => panic!("Expected File variant inside MemoryDiff"),
4121            },
4122            _ => panic!("Expected MemoryDiff variant"),
4123        }
4124    }
4125
4126    #[test]
4127    fn test_parse_sqlite_disk() {
4128        let s = "sql:db.sqlite;create=2G";
4129        let disk = DiskCliKind::from_str(s).unwrap();
4130        match disk {
4131            DiskCliKind::Sqlite {
4132                path,
4133                create_with_len,
4134            } => {
4135                assert_eq!(path, PathBuf::from("db.sqlite"));
4136                assert_eq!(create_with_len, Some(2 * 1024 * 1024 * 1024));
4137            }
4138            _ => panic!("Expected Sqlite variant"),
4139        }
4140
4141        // Test without create option
4142        let s = "sql:db.sqlite";
4143        let disk = DiskCliKind::from_str(s).unwrap();
4144        match disk {
4145            DiskCliKind::Sqlite {
4146                path,
4147                create_with_len,
4148            } => {
4149                assert_eq!(path, PathBuf::from("db.sqlite"));
4150                assert_eq!(create_with_len, None);
4151            }
4152            _ => panic!("Expected Sqlite variant"),
4153        }
4154    }
4155
4156    #[test]
4157    fn test_parse_sqlite_diff_disk() {
4158        // Test with create option
4159        let s = "sqldiff:diff.sqlite;create:file:base.img";
4160        let disk = DiskCliKind::from_str(s).unwrap();
4161        match disk {
4162            DiskCliKind::SqliteDiff { path, create, disk } => {
4163                assert_eq!(path, PathBuf::from("diff.sqlite"));
4164                assert!(create);
4165                match *disk {
4166                    DiskCliKind::File {
4167                        path,
4168                        create_with_len,
4169                        ..
4170                    } => {
4171                        assert_eq!(path, PathBuf::from("base.img"));
4172                        assert_eq!(create_with_len, None);
4173                    }
4174                    _ => panic!("Expected File variant inside SqliteDiff"),
4175                }
4176            }
4177            _ => panic!("Expected SqliteDiff variant"),
4178        }
4179
4180        // Test without create option
4181        let s = "sqldiff:diff.sqlite:file:base.img";
4182        let disk = DiskCliKind::from_str(s).unwrap();
4183        match disk {
4184            DiskCliKind::SqliteDiff { path, create, disk } => {
4185                assert_eq!(path, PathBuf::from("diff.sqlite"));
4186                assert!(!create);
4187                match *disk {
4188                    DiskCliKind::File {
4189                        path,
4190                        create_with_len,
4191                        ..
4192                    } => {
4193                        assert_eq!(path, PathBuf::from("base.img"));
4194                        assert_eq!(create_with_len, None);
4195                    }
4196                    _ => panic!("Expected File variant inside SqliteDiff"),
4197                }
4198            }
4199            _ => panic!("Expected SqliteDiff variant"),
4200        }
4201    }
4202
4203    #[test]
4204    fn test_parse_autocache_sqlite_disk() {
4205        // Test with cache path provided
4206        let disk =
4207            DiskCliKind::parse_autocache(":file:disk.vhd", Ok("/tmp/cache".to_string())).unwrap();
4208        assert!(matches!(
4209            disk,
4210            DiskCliKind::AutoCacheSqlite {
4211                cache_path,
4212                key,
4213                disk: _disk,
4214            } if cache_path == "/tmp/cache" && key.is_none()
4215        ));
4216
4217        // Test with key
4218        let disk =
4219            DiskCliKind::parse_autocache("mykey:file:disk.vhd", Ok("/tmp/cache".to_string()))
4220                .unwrap();
4221        assert!(matches!(
4222            disk,
4223            DiskCliKind::AutoCacheSqlite {
4224                cache_path,
4225                key: Some(key),
4226                disk: _disk,
4227            } if cache_path == "/tmp/cache" && key == "mykey"
4228        ));
4229
4230        // Test without cache path
4231        assert!(
4232            DiskCliKind::parse_autocache(":file:disk.vhd", Err(std::env::VarError::NotPresent),)
4233                .is_err()
4234        );
4235    }
4236
4237    #[test]
4238    fn test_parse_disk_errors() {
4239        assert!(DiskCliKind::from_str("invalid:").is_err());
4240        assert!(DiskCliKind::from_str("memory:extra").is_err());
4241
4242        // Test sqlite: without environment variable
4243        assert!(DiskCliKind::from_str("sqlite:").is_err());
4244    }
4245
4246    #[test]
4247    fn test_parse_errors() {
4248        // Invalid memory size
4249        assert!(DiskCliKind::from_str("mem:invalid").is_err());
4250
4251        // Invalid syntax for SQLiteDiff
4252        assert!(DiskCliKind::from_str("sqldiff:path").is_err());
4253
4254        // Missing OPENVMM_AUTO_CACHE_PATH for AutoCacheSqlite
4255        assert!(
4256            DiskCliKind::parse_autocache("key:file:disk.vhd", Err(std::env::VarError::NotPresent),)
4257                .is_err()
4258        );
4259
4260        // Invalid blob kind
4261        assert!(DiskCliKind::from_str("blob:invalid:url").is_err());
4262
4263        // Invalid cipher
4264        assert!(DiskCliKind::from_str("crypt:invalid:key.bin:file:disk.vhd").is_err());
4265
4266        // Invalid format for crypt (missing parts)
4267        assert!(DiskCliKind::from_str("crypt:xts-aes-256:key.bin").is_err());
4268
4269        // Invalid disk kind
4270        assert!(DiskCliKind::from_str("invalid:path").is_err());
4271
4272        // Missing create size
4273        assert!(DiskCliKind::from_str("file:disk.vhd;create=").is_err());
4274    }
4275
4276    #[test]
4277    fn test_fs_args_from_str() {
4278        let args = FsArgs::from_str("tag1,/path/to/fs").unwrap();
4279        assert_eq!(args.tag, "tag1");
4280        assert_eq!(args.path, "/path/to/fs");
4281
4282        // Test error cases
4283        assert!(FsArgs::from_str("tag1").is_err());
4284        assert!(FsArgs::from_str("tag1,/path,extra").is_err());
4285    }
4286
4287    #[test]
4288    fn test_fs_args_with_options_from_str() {
4289        let args = FsArgsWithOptions::from_str("tag1,/path/to/fs,opt1,opt2").unwrap();
4290        assert_eq!(args.tag, "tag1");
4291        assert_eq!(args.path, "/path/to/fs");
4292        assert_eq!(args.options, "opt1;opt2");
4293
4294        // Test without options
4295        let args = FsArgsWithOptions::from_str("tag1,/path/to/fs").unwrap();
4296        assert_eq!(args.tag, "tag1");
4297        assert_eq!(args.path, "/path/to/fs");
4298        assert_eq!(args.options, "");
4299
4300        // Test error case
4301        assert!(FsArgsWithOptions::from_str("tag1").is_err());
4302    }
4303
4304    #[test]
4305    fn test_serial_config_from_str() {
4306        assert_eq!(
4307            SerialConfigCli::from_str("none").unwrap(),
4308            SerialConfigCli::None
4309        );
4310        assert_eq!(
4311            SerialConfigCli::from_str("null").unwrap(),
4312            SerialConfigCli::Null
4313        );
4314        assert_eq!(
4315            SerialConfigCli::from_str("console").unwrap(),
4316            SerialConfigCli::Console
4317        );
4318        assert_eq!(
4319            SerialConfigCli::from_str("stderr").unwrap(),
4320            SerialConfigCli::Stderr
4321        );
4322
4323        // Test file config
4324        let file_config = SerialConfigCli::from_str("file=/path/to/file").unwrap();
4325        if let SerialConfigCli::File(path) = file_config {
4326            assert_eq!(path.to_str().unwrap(), "/path/to/file");
4327        } else {
4328            panic!("Expected File variant");
4329        }
4330
4331        // Test term config with name, but no specific path
4332        match SerialConfigCli::from_str("term,name=MyTerm").unwrap() {
4333            SerialConfigCli::NewConsole(None, Some(name)) => {
4334                assert_eq!(name, "MyTerm");
4335            }
4336            _ => panic!("Expected NewConsole variant with name"),
4337        }
4338
4339        // Test term config without name, but no specific path
4340        match SerialConfigCli::from_str("term").unwrap() {
4341            SerialConfigCli::NewConsole(None, None) => (),
4342            _ => panic!("Expected NewConsole variant without name"),
4343        }
4344
4345        // Test term config with name
4346        match SerialConfigCli::from_str("term=/dev/pts/0,name=MyTerm").unwrap() {
4347            SerialConfigCli::NewConsole(Some(path), Some(name)) => {
4348                assert_eq!(path.to_str().unwrap(), "/dev/pts/0");
4349                assert_eq!(name, "MyTerm");
4350            }
4351            _ => panic!("Expected NewConsole variant with name"),
4352        }
4353
4354        // Test term config without name
4355        match SerialConfigCli::from_str("term=/dev/pts/0").unwrap() {
4356            SerialConfigCli::NewConsole(Some(path), None) => {
4357                assert_eq!(path.to_str().unwrap(), "/dev/pts/0");
4358            }
4359            _ => panic!("Expected NewConsole variant without name"),
4360        }
4361
4362        // Test TCP config
4363        match SerialConfigCli::from_str("listen=tcp:127.0.0.1:1234").unwrap() {
4364            SerialConfigCli::Tcp(addr) => {
4365                assert_eq!(addr.to_string(), "127.0.0.1:1234");
4366            }
4367            _ => panic!("Expected Tcp variant"),
4368        }
4369
4370        // Test pipe config
4371        match SerialConfigCli::from_str("listen=/path/to/pipe").unwrap() {
4372            SerialConfigCli::Pipe(path) => {
4373                assert_eq!(path.to_str().unwrap(), "/path/to/pipe");
4374            }
4375            _ => panic!("Expected Pipe variant"),
4376        }
4377
4378        // Test error cases
4379        assert!(SerialConfigCli::from_str("").is_err());
4380        assert!(SerialConfigCli::from_str("unknown").is_err());
4381        assert!(SerialConfigCli::from_str("file").is_err());
4382        assert!(SerialConfigCli::from_str("listen").is_err());
4383    }
4384
4385    #[test]
4386    fn test_endpoint_config_from_str() {
4387        // Test none
4388        assert!(matches!(
4389            EndpointConfigCli::from_str("none").unwrap(),
4390            EndpointConfigCli::None
4391        ));
4392
4393        // Test consomme without cidr
4394        match EndpointConfigCli::from_str("consomme").unwrap() {
4395            EndpointConfigCli::Consomme {
4396                cidr: None,
4397                host_fwd,
4398            } => assert!(host_fwd.is_empty()),
4399            _ => panic!("Expected Consomme variant without cidr"),
4400        }
4401
4402        // Test consomme with cidr
4403        match EndpointConfigCli::from_str("consomme:192.168.0.0/24").unwrap() {
4404            EndpointConfigCli::Consomme {
4405                cidr: Some(cidr),
4406                host_fwd,
4407            } => {
4408                assert_eq!(cidr, "192.168.0.0/24");
4409                assert!(host_fwd.is_empty());
4410            }
4411            _ => panic!("Expected Consomme variant with cidr"),
4412        }
4413
4414        // Test consomme with hostfwd
4415        match EndpointConfigCli::from_str("consomme:hostfwd=udp:127.0.0.1:5000-:5000").unwrap() {
4416            EndpointConfigCli::Consomme { cidr, host_fwd } => {
4417                assert!(cidr.is_none());
4418                assert_eq!(host_fwd.len(), 1);
4419                assert_eq!(host_fwd[0].protocol, HostPortProtocolCli::Udp);
4420                assert_eq!(
4421                    host_fwd[0].host_address,
4422                    Some(std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)))
4423                );
4424                assert_eq!(host_fwd[0].host_port, 5000);
4425                assert_eq!(host_fwd[0].guest_port, 5000);
4426            }
4427            _ => panic!("Expected Consomme variant with hostfwd"),
4428        }
4429
4430        // Test consomme with cidr and hostfwd
4431        match EndpointConfigCli::from_str("consomme:10.0.0.0/24,hostfwd=tcp::2222-:22").unwrap() {
4432            EndpointConfigCli::Consomme { cidr, host_fwd } => {
4433                assert_eq!(cidr.as_deref(), Some("10.0.0.0/24"));
4434                assert_eq!(host_fwd.len(), 1);
4435                assert_eq!(host_fwd[0].protocol, HostPortProtocolCli::Tcp);
4436                assert_eq!(host_fwd[0].host_port, 2222);
4437                assert_eq!(host_fwd[0].guest_port, 22);
4438            }
4439            _ => panic!("Expected Consomme variant with cidr and hostfwd"),
4440        }
4441
4442        // Test consomme with multiple hostfwd
4443        match EndpointConfigCli::from_str("consomme:hostfwd=tcp::2222-:22,hostfwd=tcp::3389-:3389")
4444            .unwrap()
4445        {
4446            EndpointConfigCli::Consomme { cidr, host_fwd } => {
4447                assert!(cidr.is_none());
4448                assert_eq!(host_fwd.len(), 2);
4449                assert_eq!(host_fwd[0].host_port, 2222);
4450                assert_eq!(host_fwd[0].guest_port, 22);
4451                assert_eq!(host_fwd[1].host_port, 3389);
4452                assert_eq!(host_fwd[1].guest_port, 3389);
4453            }
4454            _ => panic!("Expected Consomme variant with multiple hostfwd"),
4455        }
4456
4457        // Test consomme with different host and guest ports
4458        match EndpointConfigCli::from_str("consomme:hostfwd=tcp:127.0.0.1:8080-:80").unwrap() {
4459            EndpointConfigCli::Consomme { cidr, host_fwd } => {
4460                assert!(cidr.is_none());
4461                assert_eq!(host_fwd.len(), 1);
4462                assert_eq!(host_fwd[0].protocol, HostPortProtocolCli::Tcp);
4463                assert_eq!(
4464                    host_fwd[0].host_address,
4465                    Some(std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)))
4466                );
4467                assert_eq!(host_fwd[0].host_port, 8080);
4468                assert_eq!(host_fwd[0].guest_port, 80);
4469            }
4470            _ => panic!("Expected Consomme variant with host/guest port mapping"),
4471        }
4472
4473        // Test consomme with guest address (accepted but ignored by backend)
4474        match EndpointConfigCli::from_str("consomme:hostfwd=tcp::8080-10.0.0.2:80").unwrap() {
4475            EndpointConfigCli::Consomme { cidr, host_fwd } => {
4476                assert!(cidr.is_none());
4477                assert_eq!(host_fwd[0].host_port, 8080);
4478                assert_eq!(host_fwd[0].guest_port, 80);
4479            }
4480            _ => panic!("Expected Consomme variant with guest address"),
4481        }
4482
4483        // Test consomme with IPv6 host address (bracketed)
4484        match EndpointConfigCli::from_str("consomme:hostfwd=tcp:[::1]:8080-:80").unwrap() {
4485            EndpointConfigCli::Consomme { cidr, host_fwd } => {
4486                assert!(cidr.is_none());
4487                assert_eq!(host_fwd.len(), 1);
4488                assert_eq!(host_fwd[0].protocol, HostPortProtocolCli::Tcp);
4489                assert_eq!(
4490                    host_fwd[0].host_address,
4491                    Some(std::net::IpAddr::V6(std::net::Ipv6Addr::LOCALHOST))
4492                );
4493                assert_eq!(host_fwd[0].host_port, 8080);
4494                assert_eq!(host_fwd[0].guest_port, 80);
4495            }
4496            _ => panic!("Expected Consomme variant with IPv6 hostfwd"),
4497        }
4498
4499        // Test consomme with IPv6 guest address (bracketed)
4500        match EndpointConfigCli::from_str("consomme:hostfwd=tcp::8080-[::1]:80").unwrap() {
4501            EndpointConfigCli::Consomme { cidr, host_fwd } => {
4502                assert!(cidr.is_none());
4503                assert_eq!(host_fwd[0].host_port, 8080);
4504                assert_eq!(host_fwd[0].guest_port, 80);
4505            }
4506            _ => panic!("Expected Consomme variant with IPv6 guest address"),
4507        }
4508
4509        // Test dio without id
4510        match EndpointConfigCli::from_str("dio").unwrap() {
4511            EndpointConfigCli::Dio { id: None } => (),
4512            _ => panic!("Expected Dio variant without id"),
4513        }
4514
4515        // Test dio with id
4516        match EndpointConfigCli::from_str("dio:test_id").unwrap() {
4517            EndpointConfigCli::Dio { id: Some(id) } => {
4518                assert_eq!(id, "test_id");
4519            }
4520            _ => panic!("Expected Dio variant with id"),
4521        }
4522
4523        // Test tap
4524        match EndpointConfigCli::from_str("tap:tap0").unwrap() {
4525            EndpointConfigCli::Tap { name } => {
4526                assert_eq!(name, "tap0");
4527            }
4528            _ => panic!("Expected Tap variant"),
4529        }
4530
4531        // Test error case
4532        assert!(EndpointConfigCli::from_str("invalid").is_err());
4533    }
4534
4535    #[test]
4536    fn test_nic_config_from_str() {
4537        use openvmm_defs::config::DeviceVtl;
4538
4539        // Test basic endpoint
4540        let config = NicConfigCli::from_str("none").unwrap();
4541        assert_eq!(config.vtl, DeviceVtl::Vtl0);
4542        assert!(config.max_queues.is_none());
4543        assert!(!config.underhill);
4544        assert!(config.pcie_port.is_none());
4545        assert!(matches!(config.endpoint, EndpointConfigCli::None));
4546
4547        // Test with vtl2
4548        let config = NicConfigCli::from_str("vtl2:none").unwrap();
4549        assert_eq!(config.vtl, DeviceVtl::Vtl2);
4550        assert!(config.pcie_port.is_none());
4551        assert!(matches!(config.endpoint, EndpointConfigCli::None));
4552
4553        // Test with queues
4554        let config = NicConfigCli::from_str("queues=4:none").unwrap();
4555        assert_eq!(config.max_queues, Some(4));
4556        assert!(config.pcie_port.is_none());
4557        assert!(matches!(config.endpoint, EndpointConfigCli::None));
4558
4559        // Test with underhill
4560        let config = NicConfigCli::from_str("uh:none").unwrap();
4561        assert!(config.underhill);
4562        assert!(config.pcie_port.is_none());
4563        assert!(matches!(config.endpoint, EndpointConfigCli::None));
4564
4565        // Test with pcie_port
4566        let config = NicConfigCli::from_str("pcie_port=rp0:none").unwrap();
4567        assert_eq!(config.pcie_port.unwrap(), "rp0".to_string());
4568        assert!(matches!(config.endpoint, EndpointConfigCli::None));
4569
4570        // Test error cases
4571        assert!(NicConfigCli::from_str("queues=invalid:none").is_err());
4572        assert!(NicConfigCli::from_str("uh:vtl2:none").is_err()); // uh incompatible with vtl2
4573        assert!(NicConfigCli::from_str("pcie_port=rp0:vtl2:none").is_err());
4574        assert!(NicConfigCli::from_str("uh:pcie_port=rp0:none").is_err());
4575        assert!(NicConfigCli::from_str("pcie_port=:none").is_err());
4576        assert!(NicConfigCli::from_str("pcie_port:none").is_err());
4577    }
4578
4579    #[test]
4580    fn test_parse_pcie_port_prefix() {
4581        // Successful prefix parsing
4582        let (port, rest) = parse_pcie_port_prefix("pcie_port=rp0:tag,path");
4583        assert_eq!(port.unwrap(), "rp0");
4584        assert_eq!(rest, "tag,path");
4585
4586        // No prefix
4587        let (port, rest) = parse_pcie_port_prefix("tag,path");
4588        assert!(port.is_none());
4589        assert_eq!(rest, "tag,path");
4590
4591        // Empty port name — not parsed as a prefix
4592        let (port, rest) = parse_pcie_port_prefix("pcie_port=:tag,path");
4593        assert!(port.is_none());
4594        assert_eq!(rest, "pcie_port=:tag,path");
4595
4596        // Missing colon — not parsed as a prefix
4597        let (port, rest) = parse_pcie_port_prefix("pcie_port=rp0");
4598        assert!(port.is_none());
4599        assert_eq!(rest, "pcie_port=rp0");
4600    }
4601
4602    #[test]
4603    fn test_cxl_test_device_cli_parse_valid() {
4604        let cfg = CxlTestDeviceCli::from_str("mem:1G,pcie_port=rp0").unwrap();
4605        assert_eq!(cfg.hdm_size, 1024 * 1024 * 1024);
4606        assert_eq!(cfg.pcie_port, "rp0");
4607    }
4608
4609    #[test]
4610    fn test_cxl_test_device_cli_parse_invalid() {
4611        assert!(CxlTestDeviceCli::from_str("file:disk.img,pcie_port=rp0").is_err());
4612        assert!(CxlTestDeviceCli::from_str("mem:1G").is_err());
4613        assert!(CxlTestDeviceCli::from_str("mem:1G,pcie_port=").is_err());
4614    }
4615
4616    #[test]
4617    fn test_fs_args_pcie_port() {
4618        // Without pcie_port
4619        let args = FsArgs::from_str("myfs,/path").unwrap();
4620        assert_eq!(args.tag, "myfs");
4621        assert_eq!(args.path, "/path");
4622        assert!(args.pcie_port.is_none());
4623
4624        // With pcie_port
4625        let args = FsArgs::from_str("pcie_port=rp0:myfs,/path").unwrap();
4626        assert_eq!(args.pcie_port.unwrap(), "rp0");
4627        assert_eq!(args.tag, "myfs");
4628        assert_eq!(args.path, "/path");
4629
4630        // Error: wrong number of fields
4631        assert!(FsArgs::from_str("myfs").is_err());
4632        assert!(FsArgs::from_str("pcie_port=rp0:myfs").is_err());
4633    }
4634
4635    #[test]
4636    fn test_fs_args_with_options_pcie_port() {
4637        // Without pcie_port
4638        let args = FsArgsWithOptions::from_str("myfs,/path,uid=1000").unwrap();
4639        assert_eq!(args.tag, "myfs");
4640        assert_eq!(args.path, "/path");
4641        assert_eq!(args.options, "uid=1000");
4642        assert!(args.pcie_port.is_none());
4643
4644        // With pcie_port
4645        let args = FsArgsWithOptions::from_str("pcie_port=rp0:myfs,/path,uid=1000").unwrap();
4646        assert_eq!(args.pcie_port.unwrap(), "rp0");
4647        assert_eq!(args.tag, "myfs");
4648        assert_eq!(args.path, "/path");
4649        assert_eq!(args.options, "uid=1000");
4650
4651        // Error: missing path
4652        assert!(FsArgsWithOptions::from_str("myfs").is_err());
4653    }
4654
4655    #[test]
4656    fn test_virtio_pmem_args_pcie_port() {
4657        // Without pcie_port
4658        let args = VirtioPmemArgs::from_str("/path/to/file").unwrap();
4659        assert_eq!(args.path, "/path/to/file");
4660        assert!(args.pcie_port.is_none());
4661
4662        // With pcie_port
4663        let args = VirtioPmemArgs::from_str("pcie_port=rp0:/path/to/file").unwrap();
4664        assert_eq!(args.pcie_port.unwrap(), "rp0");
4665        assert_eq!(args.path, "/path/to/file");
4666
4667        // Error: empty path
4668        assert!(VirtioPmemArgs::from_str("").is_err());
4669        assert!(VirtioPmemArgs::from_str("pcie_port=rp0:").is_err());
4670    }
4671
4672    #[test]
4673    fn test_smt_config_from_str() {
4674        assert_eq!(SmtConfigCli::from_str("auto").unwrap(), SmtConfigCli::Auto);
4675        assert_eq!(
4676            SmtConfigCli::from_str("force").unwrap(),
4677            SmtConfigCli::Force
4678        );
4679        assert_eq!(SmtConfigCli::from_str("off").unwrap(), SmtConfigCli::Off);
4680
4681        // Test error cases
4682        assert!(SmtConfigCli::from_str("invalid").is_err());
4683        assert!(SmtConfigCli::from_str("").is_err());
4684    }
4685
4686    #[test]
4687    fn test_pcat_boot_order_from_str() {
4688        // Test single device
4689        let order = PcatBootOrderCli::from_str("optical").unwrap();
4690        assert_eq!(order.0[0], PcatBootDevice::Optical);
4691
4692        // Test multiple devices
4693        let order = PcatBootOrderCli::from_str("hdd,net").unwrap();
4694        assert_eq!(order.0[0], PcatBootDevice::HardDrive);
4695        assert_eq!(order.0[1], PcatBootDevice::Network);
4696
4697        // Test error cases
4698        assert!(PcatBootOrderCli::from_str("invalid").is_err());
4699        assert!(PcatBootOrderCli::from_str("optical,optical").is_err()); // duplicate device
4700    }
4701
4702    #[test]
4703    fn test_floppy_disk_from_str() {
4704        // Test basic disk
4705        let disk = FloppyDiskCli::from_str("file:/path/to/floppy.img").unwrap();
4706        assert!(!disk.read_only);
4707        match disk.kind {
4708            DiskCliKind::File {
4709                path,
4710                create_with_len,
4711                ..
4712            } => {
4713                assert_eq!(path.to_str().unwrap(), "/path/to/floppy.img");
4714                assert_eq!(create_with_len, None);
4715            }
4716            _ => panic!("Expected File variant"),
4717        }
4718
4719        // Test with read-only flag
4720        let disk = FloppyDiskCli::from_str("file:/path/to/floppy.img,ro").unwrap();
4721        assert!(disk.read_only);
4722
4723        // Test error cases
4724        assert!(FloppyDiskCli::from_str("").is_err());
4725        assert!(FloppyDiskCli::from_str("file:/path/to/floppy.img,invalid").is_err());
4726    }
4727
4728    #[test]
4729    fn test_pcie_root_complex_from_str() {
4730        const ONE_MB: u64 = 1024 * 1024;
4731        const ONE_GB: u64 = 1024 * ONE_MB;
4732
4733        const DEFAULT_LOW_MMIO: u32 = (64 * ONE_MB) as u32;
4734        const DEFAULT_HIGH_MMIO: u64 = ONE_GB;
4735        const DEFAULT_HDM: u64 = ONE_GB;
4736        const DEFAULT_HDM_WINDOW_RESTRICTIONS: CfmwsWindowRestrictions =
4737            CfmwsWindowRestrictions::DEVICE_COHERENT;
4738
4739        assert_eq!(
4740            PcieRootComplexCli::from_str("rc0").unwrap(),
4741            PcieRootComplexCli {
4742                name: "rc0".to_string(),
4743                segment: 0,
4744                start_bus: 0,
4745                end_bus: 255,
4746                low_mmio: DEFAULT_LOW_MMIO,
4747                high_mmio: DEFAULT_HIGH_MMIO,
4748                hdm: DEFAULT_HDM,
4749                hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4750                vnode: None,
4751                low_mmio_base: None,
4752                high_mmio_base: None,
4753                preserve_bars: false,
4754            }
4755        );
4756
4757        assert_eq!(
4758            PcieRootComplexCli::from_str("rc1,segment=1").unwrap(),
4759            PcieRootComplexCli {
4760                name: "rc1".to_string(),
4761                segment: 1,
4762                start_bus: 0,
4763                end_bus: 255,
4764                low_mmio: DEFAULT_LOW_MMIO,
4765                high_mmio: DEFAULT_HIGH_MMIO,
4766                hdm: DEFAULT_HDM,
4767                hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4768                vnode: None,
4769                low_mmio_base: None,
4770                high_mmio_base: None,
4771                preserve_bars: false,
4772            }
4773        );
4774
4775        assert_eq!(
4776            PcieRootComplexCli::from_str("rc2,start_bus=32").unwrap(),
4777            PcieRootComplexCli {
4778                name: "rc2".to_string(),
4779                segment: 0,
4780                start_bus: 32,
4781                end_bus: 255,
4782                low_mmio: DEFAULT_LOW_MMIO,
4783                high_mmio: DEFAULT_HIGH_MMIO,
4784                hdm: DEFAULT_HDM,
4785                hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4786                vnode: None,
4787                low_mmio_base: None,
4788                high_mmio_base: None,
4789                preserve_bars: false,
4790            }
4791        );
4792
4793        assert_eq!(
4794            PcieRootComplexCli::from_str("rc3,end_bus=31").unwrap(),
4795            PcieRootComplexCli {
4796                name: "rc3".to_string(),
4797                segment: 0,
4798                start_bus: 0,
4799                end_bus: 31,
4800                low_mmio: DEFAULT_LOW_MMIO,
4801                high_mmio: DEFAULT_HIGH_MMIO,
4802                hdm: DEFAULT_HDM,
4803                hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4804                vnode: None,
4805                low_mmio_base: None,
4806                high_mmio_base: None,
4807                preserve_bars: false,
4808            }
4809        );
4810
4811        assert_eq!(
4812            PcieRootComplexCli::from_str("rc4,start_bus=32,end_bus=127,high_mmio=2G").unwrap(),
4813            PcieRootComplexCli {
4814                name: "rc4".to_string(),
4815                segment: 0,
4816                start_bus: 32,
4817                end_bus: 127,
4818                low_mmio: DEFAULT_LOW_MMIO,
4819                high_mmio: 2 * ONE_GB,
4820                hdm: DEFAULT_HDM,
4821                hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4822                vnode: None,
4823                low_mmio_base: None,
4824                high_mmio_base: None,
4825                preserve_bars: false,
4826            }
4827        );
4828
4829        assert_eq!(
4830            PcieRootComplexCli::from_str("rc5,segment=2,start_bus=32,end_bus=127").unwrap(),
4831            PcieRootComplexCli {
4832                name: "rc5".to_string(),
4833                segment: 2,
4834                start_bus: 32,
4835                end_bus: 127,
4836                low_mmio: DEFAULT_LOW_MMIO,
4837                high_mmio: DEFAULT_HIGH_MMIO,
4838                hdm: DEFAULT_HDM,
4839                hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4840                vnode: None,
4841                low_mmio_base: None,
4842                high_mmio_base: None,
4843                preserve_bars: false,
4844            }
4845        );
4846
4847        assert_eq!(
4848            PcieRootComplexCli::from_str("rc6,low_mmio=1M,high_mmio=64G").unwrap(),
4849            PcieRootComplexCli {
4850                name: "rc6".to_string(),
4851                segment: 0,
4852                start_bus: 0,
4853                end_bus: 255,
4854                low_mmio: ONE_MB as u32,
4855                high_mmio: 64 * ONE_GB,
4856                hdm: DEFAULT_HDM,
4857                hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4858                vnode: None,
4859                low_mmio_base: None,
4860                high_mmio_base: None,
4861                preserve_bars: false,
4862            }
4863        );
4864
4865        assert_eq!(
4866            PcieRootComplexCli::from_str("rc7,hdm=2G").unwrap(),
4867            PcieRootComplexCli {
4868                name: "rc7".to_string(),
4869                segment: 0,
4870                start_bus: 0,
4871                end_bus: 255,
4872                low_mmio: DEFAULT_LOW_MMIO,
4873                high_mmio: DEFAULT_HIGH_MMIO,
4874                hdm: 2 * ONE_GB,
4875                hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4876                vnode: None,
4877                low_mmio_base: None,
4878                high_mmio_base: None,
4879                preserve_bars: false,
4880            }
4881        );
4882
4883        assert_eq!(
4884            PcieRootComplexCli::from_str("rc8,hdm_window_restrictions=0x21").unwrap(),
4885            PcieRootComplexCli {
4886                name: "rc8".to_string(),
4887                segment: 0,
4888                start_bus: 0,
4889                end_bus: 255,
4890                low_mmio: DEFAULT_LOW_MMIO,
4891                high_mmio: DEFAULT_HIGH_MMIO,
4892                hdm: DEFAULT_HDM,
4893                hdm_window_restrictions: CfmwsWindowRestrictions::try_from_bits(0x21).unwrap(),
4894                vnode: None,
4895                low_mmio_base: None,
4896                high_mmio_base: None,
4897                preserve_bars: false,
4898            }
4899        );
4900
4901        // Error cases
4902        assert!(PcieRootComplexCli::from_str("").is_err());
4903        assert!(PcieRootComplexCli::from_str("poorly,").is_err());
4904        assert!(PcieRootComplexCli::from_str("configured,complex").is_err());
4905        assert!(PcieRootComplexCli::from_str("fails,start_bus=foo").is_err());
4906        assert!(PcieRootComplexCli::from_str("fails,start_bus=32,end_bus=31").is_err());
4907        assert!(PcieRootComplexCli::from_str("rc,start_bus=256").is_err());
4908        assert!(PcieRootComplexCli::from_str("rc,end_bus=256").is_err());
4909        assert!(PcieRootComplexCli::from_str("rc,low_mmio=5G").is_err());
4910        assert!(PcieRootComplexCli::from_str("rc,low_mmio=aG").is_err());
4911        assert!(PcieRootComplexCli::from_str("rc,high_mmio=bad").is_err());
4912        assert!(PcieRootComplexCli::from_str("rc,high_mmio").is_err());
4913        assert!(PcieRootComplexCli::from_str("rc,hdm=bad").is_err());
4914        assert!(PcieRootComplexCli::from_str("rc,hdm").is_err());
4915        assert!(PcieRootComplexCli::from_str("rc,hdm_window_restrictions=bad").is_err());
4916        assert!(PcieRootComplexCli::from_str("rc,hdm_window_restrictions").is_err());
4917        assert!(PcieRootComplexCli::from_str("rc,cxl").is_err());
4918
4919        // node option
4920        assert_eq!(
4921            PcieRootComplexCli::from_str("rc9,node=1").unwrap(),
4922            PcieRootComplexCli {
4923                name: "rc9".to_string(),
4924                segment: 0,
4925                start_bus: 0,
4926                end_bus: 255,
4927                low_mmio: DEFAULT_LOW_MMIO,
4928                high_mmio: DEFAULT_HIGH_MMIO,
4929                hdm: DEFAULT_HDM,
4930                hdm_window_restrictions: DEFAULT_HDM_WINDOW_RESTRICTIONS,
4931                vnode: Some(1),
4932                low_mmio_base: None,
4933                high_mmio_base: None,
4934                preserve_bars: false,
4935            }
4936        );
4937    }
4938
4939    #[test]
4940    fn test_pcie_root_port_from_str() {
4941        assert_eq!(
4942            PcieRootPortCli::from_str("rc0:rc0rp0").unwrap(),
4943            PcieRootPortCli {
4944                root_complex_name: "rc0".to_string(),
4945                name: "rc0rp0".to_string(),
4946                devfn: None,
4947                hotplug: false,
4948                acs_capabilities_supported: None,
4949                cxl: false,
4950                pasid: false,
4951            }
4952        );
4953
4954        assert_eq!(
4955            PcieRootPortCli::from_str("my_rc:port2").unwrap(),
4956            PcieRootPortCli {
4957                root_complex_name: "my_rc".to_string(),
4958                name: "port2".to_string(),
4959                devfn: None,
4960                hotplug: false,
4961                acs_capabilities_supported: None,
4962                cxl: false,
4963                pasid: false,
4964            }
4965        );
4966
4967        // Test with hotplug flag
4968        assert_eq!(
4969            PcieRootPortCli::from_str("my_rc:port2,hotplug").unwrap(),
4970            PcieRootPortCli {
4971                root_complex_name: "my_rc".to_string(),
4972                name: "port2".to_string(),
4973                devfn: None,
4974                hotplug: true,
4975                acs_capabilities_supported: None,
4976                cxl: false,
4977                pasid: false,
4978            }
4979        );
4980
4981        assert_eq!(
4982            PcieRootPortCli::from_str("my_rc:port3,acs=0").unwrap(),
4983            PcieRootPortCli {
4984                root_complex_name: "my_rc".to_string(),
4985                name: "port3".to_string(),
4986                devfn: None,
4987                hotplug: false,
4988                acs_capabilities_supported: Some(0),
4989                cxl: false,
4990                pasid: false,
4991            }
4992        );
4993
4994        assert_eq!(
4995            PcieRootPortCli::from_str("my_rc:port3,acs=0x5f").unwrap(),
4996            PcieRootPortCli {
4997                root_complex_name: "my_rc".to_string(),
4998                name: "port3".to_string(),
4999                devfn: None,
5000                hotplug: false,
5001                acs_capabilities_supported: Some(0x005f),
5002                cxl: false,
5003                pasid: false,
5004            }
5005        );
5006
5007        assert_eq!(
5008            PcieRootPortCli::from_str("my_rc:port4,cxl").unwrap(),
5009            PcieRootPortCli {
5010                root_complex_name: "my_rc".to_string(),
5011                name: "port4".to_string(),
5012                devfn: None,
5013                hotplug: false,
5014                acs_capabilities_supported: None,
5015                cxl: true,
5016                pasid: false,
5017            }
5018        );
5019
5020        // Test addr= (device only, and device.function)
5021        assert_eq!(
5022            PcieRootPortCli::from_str("my_rc:port5,addr=5").unwrap(),
5023            PcieRootPortCli {
5024                root_complex_name: "my_rc".to_string(),
5025                name: "port5".to_string(),
5026                devfn: Some(5 << 3),
5027                hotplug: false,
5028                acs_capabilities_supported: None,
5029                cxl: false,
5030                pasid: false,
5031            }
5032        );
5033        assert_eq!(
5034            PcieRootPortCli::from_str("my_rc:port6,addr=5.1").unwrap(),
5035            PcieRootPortCli {
5036                root_complex_name: "my_rc".to_string(),
5037                name: "port6".to_string(),
5038                devfn: Some((5 << 3) | 1),
5039                hotplug: false,
5040                acs_capabilities_supported: None,
5041                cxl: false,
5042                pasid: false,
5043            }
5044        );
5045        assert_eq!(
5046            PcieRootPortCli::from_str("my_rc:port7,addr=0x1f.7").unwrap(),
5047            PcieRootPortCli {
5048                root_complex_name: "my_rc".to_string(),
5049                name: "port7".to_string(),
5050                devfn: Some(0xff),
5051                hotplug: false,
5052                acs_capabilities_supported: None,
5053                cxl: false,
5054                pasid: false,
5055            }
5056        );
5057
5058        assert_eq!(
5059            PcieRootPortCli::from_str("my_rc:port8,pasid").unwrap(),
5060            PcieRootPortCli {
5061                root_complex_name: "my_rc".to_string(),
5062                name: "port8".to_string(),
5063                devfn: None,
5064                hotplug: false,
5065                acs_capabilities_supported: None,
5066                cxl: false,
5067                pasid: true,
5068            }
5069        );
5070
5071        // Error cases
5072        assert!(PcieRootPortCli::from_str("").is_err());
5073        assert!(PcieRootPortCli::from_str("rp0").is_err());
5074        assert!(PcieRootPortCli::from_str("rp0,opt").is_err());
5075        assert!(PcieRootPortCli::from_str("rc0:rp0:rp3").is_err());
5076        assert!(PcieRootPortCli::from_str("rc0:rp0,invalid_option").is_err());
5077        assert!(PcieRootPortCli::from_str("rc0:rp0,cxl=true").is_err());
5078        assert!(PcieRootPortCli::from_str("rc0:rp0,addr=32").is_err());
5079        assert!(PcieRootPortCli::from_str("rc0:rp0,addr=0.8").is_err());
5080        assert!(PcieRootPortCli::from_str("rc0:rp0,addr=1.2.3").is_err());
5081        assert!(PcieRootPortCli::from_str("rc0:rp0,addr").is_err());
5082        assert!(PcieRootPortCli::from_str("rc0:rp0,pasid=foo").is_err());
5083    }
5084
5085    #[test]
5086    fn test_pcie_generic_initiator_from_str() {
5087        assert_eq!(
5088            PcieGenericInitiatorCli::from_str("port=rp0,node=1").unwrap(),
5089            PcieGenericInitiatorCli {
5090                port_name: "rp0".to_string(),
5091                node: 1,
5092            }
5093        );
5094
5095        // Order should not matter.
5096        assert_eq!(
5097            PcieGenericInitiatorCli::from_str("node=2,port=sw0-downstream-1").unwrap(),
5098            PcieGenericInitiatorCli {
5099                port_name: "sw0-downstream-1".to_string(),
5100                node: 2,
5101            }
5102        );
5103
5104        // Error cases
5105        assert!(PcieGenericInitiatorCli::from_str("").is_err());
5106        assert!(PcieGenericInitiatorCli::from_str("port=rp0").is_err());
5107        assert!(PcieGenericInitiatorCli::from_str("node=1").is_err());
5108        assert!(PcieGenericInitiatorCli::from_str("rp0=1").is_err());
5109        assert!(PcieGenericInitiatorCli::from_str("port=,node=1").is_err());
5110        assert!(PcieGenericInitiatorCli::from_str("port=rp0,node=x").is_err());
5111        assert!(PcieGenericInitiatorCli::from_str("port=rp0,node=1,extra").is_err());
5112    }
5113
5114    #[test]
5115    fn test_pcie_switch_from_str() {
5116        assert_eq!(
5117            GenericPcieSwitchCli::from_str("rp0:switch0").unwrap(),
5118            GenericPcieSwitchCli {
5119                port_name: "rp0".to_string(),
5120                name: "switch0".to_string(),
5121                num_downstream_ports: 4,
5122                hotplug: false,
5123                acs_capabilities_supported: None,
5124                pasid: false,
5125            }
5126        );
5127
5128        assert_eq!(
5129            GenericPcieSwitchCli::from_str("port1:my_switch,num_downstream_ports=4").unwrap(),
5130            GenericPcieSwitchCli {
5131                port_name: "port1".to_string(),
5132                name: "my_switch".to_string(),
5133                num_downstream_ports: 4,
5134                hotplug: false,
5135                acs_capabilities_supported: None,
5136                pasid: false,
5137            }
5138        );
5139
5140        assert_eq!(
5141            GenericPcieSwitchCli::from_str("rp2:sw,num_downstream_ports=8").unwrap(),
5142            GenericPcieSwitchCli {
5143                port_name: "rp2".to_string(),
5144                name: "sw".to_string(),
5145                num_downstream_ports: 8,
5146                hotplug: false,
5147                acs_capabilities_supported: None,
5148                pasid: false,
5149            }
5150        );
5151
5152        // Test hierarchical connections
5153        assert_eq!(
5154            GenericPcieSwitchCli::from_str("switch0-downstream-1:child_switch").unwrap(),
5155            GenericPcieSwitchCli {
5156                port_name: "switch0-downstream-1".to_string(),
5157                name: "child_switch".to_string(),
5158                num_downstream_ports: 4,
5159                hotplug: false,
5160                acs_capabilities_supported: None,
5161                pasid: false,
5162            }
5163        );
5164
5165        // Test hotplug flag
5166        assert_eq!(
5167            GenericPcieSwitchCli::from_str("rp0:switch0,hotplug").unwrap(),
5168            GenericPcieSwitchCli {
5169                port_name: "rp0".to_string(),
5170                name: "switch0".to_string(),
5171                num_downstream_ports: 4,
5172                hotplug: true,
5173                acs_capabilities_supported: None,
5174                pasid: false,
5175            }
5176        );
5177
5178        // Test hotplug with num_downstream_ports
5179        assert_eq!(
5180            GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=8,hotplug").unwrap(),
5181            GenericPcieSwitchCli {
5182                port_name: "rp0".to_string(),
5183                name: "switch0".to_string(),
5184                num_downstream_ports: 8,
5185                hotplug: true,
5186                acs_capabilities_supported: None,
5187                pasid: false,
5188            }
5189        );
5190
5191        assert_eq!(
5192            GenericPcieSwitchCli::from_str("rp0:switch0,acs=0").unwrap(),
5193            GenericPcieSwitchCli {
5194                port_name: "rp0".to_string(),
5195                name: "switch0".to_string(),
5196                num_downstream_ports: 4,
5197                hotplug: false,
5198                acs_capabilities_supported: Some(0),
5199                pasid: false,
5200            }
5201        );
5202
5203        assert_eq!(
5204            GenericPcieSwitchCli::from_str("rp0:switch0,acs=95").unwrap(),
5205            GenericPcieSwitchCli {
5206                port_name: "rp0".to_string(),
5207                name: "switch0".to_string(),
5208                num_downstream_ports: 4,
5209                hotplug: false,
5210                acs_capabilities_supported: Some(95),
5211                pasid: false,
5212            }
5213        );
5214
5215        assert_eq!(
5216            GenericPcieSwitchCli::from_str("rp0:switch0,pasid").unwrap(),
5217            GenericPcieSwitchCli {
5218                port_name: "rp0".to_string(),
5219                name: "switch0".to_string(),
5220                num_downstream_ports: 4,
5221                hotplug: false,
5222                acs_capabilities_supported: None,
5223                pasid: true,
5224            }
5225        );
5226
5227        // Error cases
5228        assert!(GenericPcieSwitchCli::from_str("").is_err());
5229        assert!(GenericPcieSwitchCli::from_str("switch0").is_err());
5230        assert!(GenericPcieSwitchCli::from_str("rp0:switch0:extra").is_err());
5231        assert!(GenericPcieSwitchCli::from_str("rp0:switch0,invalid_opt=value").is_err());
5232        assert!(GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=bad").is_err());
5233        assert!(GenericPcieSwitchCli::from_str("rp0:switch0,num_downstream_ports=").is_err());
5234        assert!(GenericPcieSwitchCli::from_str("rp0:switch0,invalid_flag").is_err());
5235        assert!(GenericPcieSwitchCli::from_str("rp0:switch0,pasid=bar").is_err());
5236    }
5237
5238    #[test]
5239    fn test_pcie_remote_from_str() {
5240        // Basic port name only
5241        assert_eq!(
5242            PcieRemoteCli::from_str("rc0rp0").unwrap(),
5243            PcieRemoteCli {
5244                port_name: "rc0rp0".to_string(),
5245                socket_addr: None,
5246                hu: 0,
5247                controller: 0,
5248            }
5249        );
5250
5251        // With socket address
5252        assert_eq!(
5253            PcieRemoteCli::from_str("rc0rp0,socket=localhost:22567").unwrap(),
5254            PcieRemoteCli {
5255                port_name: "rc0rp0".to_string(),
5256                socket_addr: Some("localhost:22567".to_string()),
5257                hu: 0,
5258                controller: 0,
5259            }
5260        );
5261
5262        // With all options
5263        assert_eq!(
5264            PcieRemoteCli::from_str("myport,socket=localhost:22568,hu=1,controller=2").unwrap(),
5265            PcieRemoteCli {
5266                port_name: "myport".to_string(),
5267                socket_addr: Some("localhost:22568".to_string()),
5268                hu: 1,
5269                controller: 2,
5270            }
5271        );
5272
5273        // Only hu and controller
5274        assert_eq!(
5275            PcieRemoteCli::from_str("port0,hu=5,controller=3").unwrap(),
5276            PcieRemoteCli {
5277                port_name: "port0".to_string(),
5278                socket_addr: None,
5279                hu: 5,
5280                controller: 3,
5281            }
5282        );
5283
5284        // Error cases
5285        assert!(PcieRemoteCli::from_str("").is_err());
5286        assert!(PcieRemoteCli::from_str("port,socket=").is_err());
5287        assert!(PcieRemoteCli::from_str("port,hu=").is_err());
5288        assert!(PcieRemoteCli::from_str("port,hu=bad").is_err());
5289        assert!(PcieRemoteCli::from_str("port,controller=").is_err());
5290        assert!(PcieRemoteCli::from_str("port,controller=bad").is_err());
5291        assert!(PcieRemoteCli::from_str("port,unknown=value").is_err());
5292    }
5293
5294    #[test]
5295    fn test_parse_memory_units() {
5296        assert_eq!(parse_memory("64G").unwrap(), 64 * 1024 * 1024 * 1024);
5297        assert_eq!(parse_memory("64GB").unwrap(), 64 * 1024 * 1024 * 1024);
5298        assert_eq!(parse_memory("3MB").unwrap(), 3 * 1024 * 1024);
5299        assert_eq!(parse_memory("512KB").unwrap(), 512 * 1024);
5300        assert!(parse_memory("3MiB").is_err());
5301    }
5302
5303    #[test]
5304    fn test_memory_config_size_only() {
5305        assert_eq!(
5306            parse_memory_config("64G").unwrap(),
5307            MemoryCli {
5308                size: Some(vmm_cli::MemorySize(64 * 1024 * 1024 * 1024)),
5309                ..Default::default()
5310            }
5311        );
5312    }
5313
5314    #[test]
5315    fn test_memory_config_key_value() {
5316        assert_eq!(
5317            parse_memory_config("size=2G,shared=off,prefetch=on,thp=on").unwrap(),
5318            MemoryCli {
5319                size: Some(vmm_cli::MemorySize(2 * 1024 * 1024 * 1024)),
5320                shared: Some(false),
5321                prefetch: true,
5322                transparent_hugepages: Some(true),
5323                ..Default::default()
5324            }
5325        );
5326
5327        assert_eq!(
5328            parse_memory_config("size=4GB,hugepages=on,hugepage_size=2MB").unwrap(),
5329            MemoryCli {
5330                size: Some(vmm_cli::MemorySize(4 * 1024 * 1024 * 1024)),
5331                hugepages: true,
5332                hugepage_size: Some(vmm_cli::MemorySize(2 * 1024 * 1024)),
5333                ..Default::default()
5334            }
5335        );
5336
5337        assert_eq!(
5338            parse_memory_config("file=/tmp/memory.bin").unwrap(),
5339            MemoryCli {
5340                file: Some(PathBuf::from("/tmp/memory.bin")),
5341                ..Default::default()
5342            }
5343        );
5344    }
5345
5346    #[test]
5347    fn test_memory_config_rejects_invalid_combinations() {
5348        assert!(parse_memory_config("size=1G,size=2G").is_err());
5349        assert!(parse_memory_config("hugepage_size=2M").is_err());
5350        assert!(parse_memory_config("hugepages=on,shared=off").is_err());
5351        assert!(parse_memory_config("hugepages=on,file=/tmp/memory.bin").is_err());
5352
5353        // Semantic validation of the hugepage size happens in the memory
5354        // builder, not in CLI parsing.
5355        assert_eq!(
5356            parse_memory_config("hugepages=on,hugepage_size=3MB")
5357                .unwrap()
5358                .hugepage_size,
5359            Some(vmm_cli::MemorySize(3 * 1024 * 1024))
5360        );
5361    }
5362
5363    #[test]
5364    fn test_smbios_requires_type() {
5365        // `type=` is mandatory; there is no default.
5366        assert!(parse_smbios("manufacturer=Contoso,family=Foo").is_err());
5367    }
5368
5369    #[test]
5370    fn test_smbios_all_system_keys() {
5371        let parsed = parse_smbios(
5372            "type=1,manufacturer=M,product=P,version=V,serial=S,sku=K,family=F,\
5373             uuid=12345678-9abc-def0-1234-56789abcdef0",
5374        )
5375        .unwrap();
5376        assert_eq!(
5377            parsed,
5378            SmbiosCli {
5379                system: SmbiosSystemCli {
5380                    manufacturer: Some("M".to_string()),
5381                    product_name: Some("P".to_string()),
5382                    version: Some("V".to_string()),
5383                    serial_number: Some("S".to_string()),
5384                    sku_number: Some("K".to_string()),
5385                    family: Some("F".to_string()),
5386                    uuid: Some(SmbiosUuid::Fixed(
5387                        "12345678-9abc-def0-1234-56789abcdef0"
5388                            .parse::<Guid>()
5389                            .unwrap()
5390                    )),
5391                },
5392                ..Default::default()
5393            }
5394        );
5395    }
5396
5397    #[test]
5398    fn test_smbios_bios_keys() {
5399        let parsed =
5400            parse_smbios("type=0,vendor=Contoso,version=1.0,date=01/01/2026,release=4.1").unwrap();
5401        assert_eq!(
5402            parsed,
5403            SmbiosCli {
5404                bios: SmbiosBiosCli {
5405                    vendor: Some("Contoso".to_string()),
5406                    version: Some("1.0".to_string()),
5407                    release_date: Some("01/01/2026".to_string()),
5408                    release: Some(SmbiosRelease(4, 1)),
5409                },
5410                ..Default::default()
5411            }
5412        );
5413    }
5414
5415    #[test]
5416    fn test_smbios_release_parsing() {
5417        // Valid MAJOR.MINOR.
5418        assert_eq!(
5419            parse_smbios("type=0,release=4.1").unwrap().bios.release,
5420            Some(SmbiosRelease(4, 1))
5421        );
5422        assert_eq!(
5423            parse_smbios("type=0,release=255.0").unwrap().bios.release,
5424            Some(SmbiosRelease(255, 0))
5425        );
5426        // Missing the dot, extra components, out-of-range, and non-numeric all
5427        // error.
5428        assert!(parse_smbios("type=0,release=4").is_err());
5429        assert!(parse_smbios("type=0,release=4.1.0").is_err());
5430        assert!(parse_smbios("type=0,release=256.0").is_err());
5431        assert!(parse_smbios("type=0,release=x.y").is_err());
5432        // `release` is a type=0 key only.
5433        assert!(parse_smbios("type=1,release=4.1").is_err());
5434    }
5435
5436    #[test]
5437    fn test_smbios_uuid() {
5438        // An explicit GUID parses to `Fixed`.
5439        assert_eq!(
5440            parse_smbios("type=1,uuid=12345678-9abc-def0-1234-56789abcdef0")
5441                .unwrap()
5442                .system
5443                .uuid,
5444            Some(SmbiosUuid::Fixed(
5445                "12345678-9abc-def0-1234-56789abcdef0"
5446                    .parse::<Guid>()
5447                    .unwrap()
5448            ))
5449        );
5450        // The literal `random` parses to `Random`.
5451        assert_eq!(
5452            parse_smbios("type=1,uuid=random").unwrap().system.uuid,
5453            Some(SmbiosUuid::Random)
5454        );
5455    }
5456
5457    #[test]
5458    fn test_smbios_version_disambiguated_by_type() {
5459        // `version` belongs to BIOS under type=0 and System under type=1.
5460        let bios = parse_smbios("type=0,version=1.0").unwrap();
5461        assert_eq!(bios.bios.version.as_deref(), Some("1.0"));
5462        assert_eq!(bios.system.version, None);
5463
5464        let system = parse_smbios("type=1,version=2.0").unwrap();
5465        assert_eq!(system.system.version.as_deref(), Some("2.0"));
5466        assert_eq!(system.bios.version, None);
5467    }
5468
5469    #[test]
5470    fn test_smbios_rejects_invalid() {
5471        // Missing type.
5472        assert!(parse_smbios("manufacturer=M").is_err());
5473        // Unknown key for the type.
5474        assert!(parse_smbios("type=0,manufacturer=M").is_err());
5475        assert!(parse_smbios("type=1,nonsense=x").is_err());
5476        // Unknown / unsupported type.
5477        assert!(parse_smbios("type=2,vendor=M").is_err());
5478        assert!(parse_smbios("type=bad,vendor=M").is_err());
5479        // Malformed key=value.
5480        assert!(parse_smbios("type=1,manufacturer").is_err());
5481        assert!(parse_smbios("=value").is_err());
5482        assert!(parse_smbios("type=1,manufacturer=").is_err());
5483        // Duplicate key within one argument.
5484        assert!(parse_smbios("type=1,manufacturer=A,manufacturer=B").is_err());
5485        assert!(parse_smbios("type=0,type=1,vendor=M").is_err());
5486        // Invalid UUID.
5487        assert!(parse_smbios("type=1,uuid=not-a-guid").is_err());
5488    }
5489
5490    #[test]
5491    fn test_smbios_merge_across_arguments() {
5492        let mut merged = SmbiosCli::default();
5493        merged
5494            .merge(parse_smbios("type=0,vendor=Contoso").unwrap())
5495            .unwrap();
5496        merged
5497            .merge(parse_smbios("type=1,manufacturer=M,family=F").unwrap())
5498            .unwrap();
5499        assert_eq!(
5500            merged,
5501            SmbiosCli {
5502                bios: SmbiosBiosCli {
5503                    vendor: Some("Contoso".to_string()),
5504                    ..Default::default()
5505                },
5506                system: SmbiosSystemCli {
5507                    manufacturer: Some("M".to_string()),
5508                    family: Some("F".to_string()),
5509                    ..Default::default()
5510                },
5511            }
5512        );
5513    }
5514
5515    #[test]
5516    fn test_smbios_merge_rejects_conflicts() {
5517        let mut merged = SmbiosCli::default();
5518        merged
5519            .merge(parse_smbios("type=1,manufacturer=A").unwrap())
5520            .unwrap();
5521        // The same field set by a second argument is a hard error.
5522        assert!(
5523            merged
5524                .merge(parse_smbios("type=1,manufacturer=B").unwrap())
5525                .is_err()
5526        );
5527    }
5528
5529    #[test]
5530    fn test_smbios_arg_repeatable() {
5531        let opt = Options::try_parse_from([
5532            "openvmm",
5533            "--smbios",
5534            "type=1,manufacturer=Contoso",
5535            "--smbios",
5536            "type=0,vendor=Acme",
5537        ])
5538        .unwrap();
5539        assert_eq!(opt.smbios.len(), 2);
5540        assert_eq!(
5541            opt.smbios[0].system.manufacturer.as_deref(),
5542            Some("Contoso")
5543        );
5544        assert_eq!(opt.smbios[1].bios.vendor.as_deref(), Some("Acme"));
5545    }
5546
5547    #[test]
5548    fn test_memory_options_merge_legacy_aliases() {
5549        let opt = Options::try_parse_from([
5550            "openvmm",
5551            "--memory",
5552            "2G",
5553            "--prefetch",
5554            "--private-memory",
5555            "--thp",
5556        ])
5557        .unwrap();
5558        opt.validate_memory_options().unwrap();
5559        assert_eq!(opt.memory_size(), 2 * 1024 * 1024 * 1024);
5560        assert!(opt.prefetch_memory());
5561        assert!(opt.private_memory());
5562        assert!(opt.transparent_hugepages());
5563    }
5564
5565    #[test]
5566    fn test_serial_debugger_mode_option_parsed() {
5567        // No COM port configured: no debugger mode.
5568        let opt = Options::try_parse_from(["openvmm"]).unwrap();
5569        assert!(opt.com1.is_none());
5570
5571        // A plain backend is not in debugger mode.
5572        let opt = Options::try_parse_from(["openvmm", "--com1", "none"]).unwrap();
5573        let com1 = opt.com1.unwrap();
5574        assert!(!com1.debugger_mode);
5575        assert_eq!(com1.backend, SerialConfigCli::None);
5576
5577        // The `debugger-mode:` prefix enables debugger mode for just that port,
5578        // and the remainder still parses as the backend.
5579        let opt = Options::try_parse_from([
5580            "openvmm",
5581            "--com1",
5582            "debugger-mode:listen=/tmp/kd",
5583            "--com2",
5584            "none",
5585        ])
5586        .unwrap();
5587        let com1 = opt.com1.unwrap();
5588        assert!(com1.debugger_mode);
5589        assert_eq!(com1.backend, SerialConfigCli::Pipe("/tmp/kd".into()));
5590        // Other ports remain independent (not in debugger mode).
5591        assert!(!opt.com2.unwrap().debugger_mode);
5592
5593        // The prefix must not eat colons in the backend (e.g. a tcp address).
5594        let opt = Options::try_parse_from([
5595            "openvmm",
5596            "--com1",
5597            "debugger-mode:listen=tcp:127.0.0.1:5555",
5598        ])
5599        .unwrap();
5600        let com1 = opt.com1.unwrap();
5601        assert!(com1.debugger_mode);
5602        assert_eq!(
5603            com1.backend,
5604            SerialConfigCli::Tcp("127.0.0.1:5555".parse().unwrap())
5605        );
5606    }
5607
5608    #[test]
5609    fn test_no_hv_requires_no_vmbus() {
5610        assert!(Options::try_parse_from(["openvmm", "--no-hv"]).is_err());
5611
5612        let opt = Options::try_parse_from(["openvmm", "--uefi", "--no-hv", "--no-vmbus"]).unwrap();
5613        assert!(opt.no_hv);
5614        assert!(opt.no_vmbus);
5615
5616        assert!(
5617            Options::try_parse_from(["openvmm", "--uefi", "--no-hv", "--no-vmbus", "--hv"])
5618                .is_err()
5619        );
5620    }
5621
5622    #[test]
5623    fn test_memory_options_allow_legacy_thp_with_new_private_memory() {
5624        let opt = Options::try_parse_from(["openvmm", "--memory", "shared=off", "--thp"]).unwrap();
5625        opt.validate_memory_options().unwrap();
5626        assert!(opt.private_memory());
5627        assert!(opt.transparent_hugepages());
5628    }
5629
5630    #[test]
5631    fn test_memory_options_reject_conflicting_legacy_aliases() {
5632        let opt = Options::try_parse_from(["openvmm", "--memory", "shared=on", "--private-memory"])
5633            .unwrap();
5634        assert!(opt.validate_memory_options().is_err());
5635    }
5636
5637    #[test]
5638    fn test_isolation_options_reject_snp_uefi() {
5639        let opt = Options::try_parse_from(["openvmm", "--isolation", "snp", "--uefi"]).unwrap();
5640
5641        assert_eq!(
5642            opt.validate_isolation_options().unwrap_err().to_string(),
5643            "SNP isolation currently only supports Linux direct boot"
5644        );
5645    }
5646
5647    #[test]
5648    fn test_isolation_options_reject_snp_hugepages() {
5649        for args in [
5650            vec![
5651                "openvmm",
5652                "--isolation",
5653                "snp",
5654                "--memory",
5655                "size=1G,hugepages=on",
5656            ],
5657            vec![
5658                "openvmm",
5659                "--isolation",
5660                "snp",
5661                "--numa",
5662                "size=1G,hugepages=on",
5663            ],
5664        ] {
5665            let opt = Options::try_parse_from(args).unwrap();
5666            assert_eq!(
5667                opt.validate_isolation_options().unwrap_err().to_string(),
5668                "SNP isolation currently does not support hugetlb memory"
5669            );
5670        }
5671    }
5672
5673    #[test]
5674    fn test_restricted_injection_requires_snp() {
5675        let opt = Options::try_parse_from(["openvmm", "--snp-restricted-injection"]).unwrap();
5676
5677        assert_eq!(
5678            opt.validate_isolation_options().unwrap_err().to_string(),
5679            "--snp-restricted-injection requires --isolation snp"
5680        );
5681
5682        let opt = Options::try_parse_from([
5683            "openvmm",
5684            "--isolation",
5685            "snp",
5686            "--snp-restricted-injection",
5687        ])
5688        .unwrap();
5689        assert_eq!(
5690            opt.validate_isolation_options().unwrap_err().to_string(),
5691            "--snp-restricted-injection requires --hypervisor mshv"
5692        );
5693
5694        let opt = Options::try_parse_from([
5695            "openvmm",
5696            "--hypervisor",
5697            "mshv",
5698            "--isolation",
5699            "snp",
5700            "--snp-restricted-injection",
5701        ])
5702        .unwrap();
5703        opt.validate_isolation_options().unwrap();
5704
5705        let opt = Options::try_parse_from([
5706            "openvmm",
5707            "--hypervisor",
5708            "mshv",
5709            "--isolation",
5710            "snp",
5711            "--snp-restricted-injection",
5712            "--pcat",
5713        ])
5714        .unwrap();
5715        assert_eq!(
5716            opt.validate_isolation_options().unwrap_err().to_string(),
5717            "--snp-restricted-injection requires Linux direct boot"
5718        );
5719
5720        let opt = Options::try_parse_from([
5721            "openvmm",
5722            "--hypervisor",
5723            "mshv",
5724            "--isolation",
5725            "snp",
5726            "--snp-restricted-injection",
5727            "--restore-snapshot",
5728            "snapshot",
5729        ])
5730        .unwrap();
5731        assert_eq!(
5732            opt.validate_isolation_options().unwrap_err().to_string(),
5733            "--snp-restricted-injection requires Linux direct boot"
5734        );
5735    }
5736
5737    #[test]
5738    fn test_isolation_options_allow_vbs_uefi() {
5739        let opt = Options::try_parse_from(["openvmm", "--isolation", "vbs", "--uefi"]).unwrap();
5740
5741        opt.validate_isolation_options().unwrap();
5742    }
5743
5744    #[test]
5745    fn test_isolation_options_allow_vbs_hugepages() {
5746        let opt = Options::try_parse_from([
5747            "openvmm",
5748            "--isolation",
5749            "vbs",
5750            "--memory",
5751            "size=1G,hugepages=on",
5752        ])
5753        .unwrap();
5754
5755        opt.validate_isolation_options().unwrap();
5756    }
5757
5758    #[test]
5759    fn test_igvm_requires_firmware_and_personality() {
5760        for (options, missing) in [
5761            ("firmware=guest.igvm", "personality"),
5762            ("personality=openhcl", "firmware"),
5763        ] {
5764            for extra in [vec![], vec!["--hv", "--vtl2"]] {
5765                let mut args = vec!["openvmm", "--igvm", options];
5766                args.extend(extra);
5767                let err = Options::try_parse_from(args).err().unwrap();
5768                assert_eq!(err.kind(), clap::error::ErrorKind::ValueValidation);
5769                assert!(err.to_string().contains(missing));
5770                assert!(err.to_string().contains("required"));
5771            }
5772        }
5773        let opt = Options::try_parse_from(["openvmm"]).unwrap();
5774        opt.validate_igvm_options().unwrap();
5775    }
5776
5777    #[test]
5778    fn test_igvm_personality_values() {
5779        for (value, expected) in [
5780            ("openhcl", IgvmPersonalityCli::Openhcl),
5781            ("uefi", IgvmPersonalityCli::Uefi),
5782            ("linux-direct", IgvmPersonalityCli::LinuxDirect),
5783        ] {
5784            let options = format!("firmware=guest.igvm,personality={value}");
5785            let mut args = vec!["openvmm", "--igvm", &options];
5786            if expected == IgvmPersonalityCli::Openhcl {
5787                args.extend(["--hv", "--vtl2"]);
5788            }
5789            let opt = Options::try_parse_from(args).unwrap();
5790            opt.validate_igvm_options().unwrap();
5791            assert_eq!(
5792                opt.igvm,
5793                Some(IgvmCli {
5794                    firmware: PathBuf::from("guest.igvm"),
5795                    personality: expected,
5796                })
5797            );
5798        }
5799    }
5800
5801    #[test]
5802    fn test_igvm_options_accept_reordered_keys_and_windows_paths() {
5803        let opt = Options::try_parse_from([
5804            "openvmm",
5805            "--igvm",
5806            r"personality=uefi,firmware=C:\VM Images\guest=1.igvm",
5807        ])
5808        .unwrap();
5809        assert_eq!(
5810            opt.igvm,
5811            Some(IgvmCli {
5812                firmware: PathBuf::from(r"C:\VM Images\guest=1.igvm"),
5813                personality: IgvmPersonalityCli::Uefi,
5814            })
5815        );
5816    }
5817
5818    #[test]
5819    fn test_igvm_options_reject_invalid_values() {
5820        for options in [
5821            "",
5822            "guest.igvm",
5823            "firmware=,personality=uefi",
5824            "firmware=guest.igvm,personality=",
5825            "firmware=guest.igvm,personality=pcat",
5826            "firmware=guest.igvm,personality=UEFI",
5827            "firmware=guest.igvm,personality=uefi,unknown=on",
5828            "firmware=guest.igvm,firmware=other.igvm,personality=uefi",
5829            "firmware=guest.igvm,personality=uefi,personality=linux-direct",
5830        ] {
5831            let err = Options::try_parse_from(["openvmm", "--igvm", options])
5832                .err()
5833                .unwrap();
5834            assert_eq!(err.kind(), clap::error::ErrorKind::ValueValidation);
5835        }
5836    }
5837
5838    #[test]
5839    fn test_igvm_rejects_separate_personality_flag() {
5840        let err = Options::try_parse_from([
5841            "openvmm",
5842            "--igvm",
5843            "firmware=guest.igvm,personality=uefi",
5844            "--igvm-personality",
5845            "uefi",
5846        ])
5847        .err()
5848        .unwrap();
5849        assert_eq!(err.kind(), clap::error::ErrorKind::UnknownArgument);
5850    }
5851
5852    #[test]
5853    fn test_openhcl_igvm_personality() {
5854        let opt = Options::try_parse_from([
5855            "openvmm",
5856            "--igvm",
5857            "firmware=guest.igvm,personality=openhcl",
5858            "--hv",
5859            "--vtl2",
5860        ])
5861        .unwrap();
5862        opt.validate_igvm_options().unwrap();
5863        assert_eq!(
5864            opt.igvm.as_ref().unwrap().personality,
5865            IgvmPersonalityCli::Openhcl
5866        );
5867        assert!(opt.effective_uefi().unwrap().is_none());
5868    }
5869
5870    #[test]
5871    fn test_openhcl_igvm_requires_explicit_vtl2_and_hv() {
5872        for extra in [vec![], vec!["--hv"], vec!["--vtl2"]] {
5873            let mut args = vec![
5874                "openvmm",
5875                "--igvm",
5876                "firmware=guest.igvm,personality=openhcl",
5877            ];
5878            args.extend(extra);
5879            match Options::try_parse_from(args) {
5880                Ok(opt) => assert_eq!(
5881                    opt.validate_igvm_options().unwrap_err().to_string(),
5882                    "--igvm personality=openhcl requires --hv --vtl2"
5883                ),
5884                Err(err) => {
5885                    assert_eq!(err.kind(), clap::error::ErrorKind::MissingRequiredArgument);
5886                    assert!(err.to_string().contains("--hv"));
5887                }
5888            }
5889        }
5890    }
5891
5892    #[test]
5893    fn test_non_openhcl_igvm_personalities_reject_vtl2_and_uefi() {
5894        for personality in ["uefi", "linux-direct"] {
5895            let options = format!("firmware=guest.igvm,personality={personality}");
5896            for extra in [vec!["--uefi"], vec!["--hv", "--vtl2"]] {
5897                let mut args = vec!["openvmm", "--igvm", &options];
5898                args.extend(extra);
5899                let opt = Options::try_parse_from(args).unwrap();
5900                assert!(opt.validate_igvm_options().is_err());
5901            }
5902        }
5903    }
5904
5905    #[test]
5906    fn test_openhcl_igvm_accepts_uefi_settings() {
5907        let opt = Options::try_parse_from([
5908            "openvmm",
5909            "--igvm",
5910            "firmware=guest.igvm,personality=openhcl",
5911            "--hv",
5912            "--vtl2",
5913            "--uefi",
5914            "console=com1,disable_frontpage",
5915        ])
5916        .unwrap();
5917        opt.validate_igvm_options().unwrap();
5918        let uefi = opt.effective_uefi().unwrap().unwrap();
5919        assert!(matches!(uefi.console, Some(UefiConsoleModeCli::Com1)));
5920        assert!(uefi.disable_frontpage);
5921    }
5922
5923    #[test]
5924    fn test_igvm_personality_conflicts_with_pcat() {
5925        for personality in ["openhcl", "uefi", "linux-direct"] {
5926            let options = format!("firmware=guest.igvm,personality={personality}");
5927            assert!(Options::try_parse_from(["openvmm", "--igvm", &options, "--pcat"]).is_err());
5928        }
5929    }
5930
5931    #[test]
5932    fn test_pidfile_option_parsed() {
5933        let opt = Options::try_parse_from(["openvmm", "--pidfile", "/tmp/test.pid"]).unwrap();
5934        assert_eq!(opt.pidfile, Some(PathBuf::from("/tmp/test.pid")));
5935    }
5936
5937    #[test]
5938    fn test_tpm_version_option() {
5939        let opt = Options::try_parse_from(["openvmm"]).unwrap();
5940        assert_eq!(opt.tpm, None);
5941
5942        let opt = Options::try_parse_from(["openvmm", "--tpm"]).unwrap();
5943        assert_eq!(opt.tpm, Some(TpmVersionCli::V185));
5944
5945        let opt = Options::try_parse_from(["openvmm", "--tpm", "--uefi"]).unwrap();
5946        assert_eq!(opt.tpm, Some(TpmVersionCli::V185));
5947        assert!(opt.uefi.is_some());
5948
5949        let opt = Options::try_parse_from(["openvmm", "--tpm", "138"]).unwrap();
5950        assert_eq!(opt.tpm, Some(TpmVersionCli::V138));
5951
5952        let opt = Options::try_parse_from(["openvmm", "--tpm=185"]).unwrap();
5953        assert_eq!(opt.tpm, Some(TpmVersionCli::V185));
5954
5955        let opt = Options::try_parse_from(["openvmm", "--tpm", "1.38"]).unwrap();
5956        assert_eq!(opt.tpm, Some(TpmVersionCli::V138));
5957
5958        assert!(Options::try_parse_from(["openvmm", "--tpm", "137"]).is_err());
5959    }
5960
5961    #[test]
5962    fn test_uefi_options() {
5963        let options = Options::try_parse_from(["openvmm", "--uefi"]).unwrap();
5964        assert!(options.effective_uefi().unwrap().is_some());
5965
5966        let options = Options::try_parse_from([
5967            "openvmm",
5968            "--uefi",
5969            "firmware=foo/bar,debug,force_firmware_version,console=com1,diagnostics=full",
5970        ])
5971        .unwrap();
5972        let uefi = options.effective_uefi().unwrap().unwrap();
5973        assert_eq!(uefi.firmware, Some("foo/bar".into()));
5974        assert!(uefi.debug);
5975        assert!(uefi.force_firmware_version);
5976        assert!(matches!(uefi.console, Some(UefiConsoleModeCli::Com1)));
5977        assert!(matches!(
5978            uefi.diagnostics,
5979            Some(EfiDiagnosticsLogLevelCli::Full)
5980        ));
5981
5982        let options = Options::try_parse_from([
5983            "openvmm",
5984            "--uefi",
5985            "--uefi-firmware",
5986            "legacy.fd",
5987            "--uefi-debug",
5988        ])
5989        .unwrap();
5990        let uefi = options.effective_uefi().unwrap().unwrap();
5991        assert_eq!(uefi.firmware, Some("legacy.fd".into()));
5992        assert!(uefi.debug);
5993
5994        assert!(Options::try_parse_from(["openvmm", "--uefi", "unknown"]).is_err());
5995        let options = Options::try_parse_from(["openvmm", "--uefi", "debug=on"]).unwrap();
5996        assert!(options.effective_uefi().unwrap().unwrap().debug);
5997        let options = Options::try_parse_from(["openvmm", "--uefi", "debug=off"]).unwrap();
5998        assert!(!options.effective_uefi().unwrap().unwrap().debug);
5999        assert!(Options::try_parse_from(["openvmm", "--uefi", "debug,debug"]).is_err());
6000
6001        let options = Options::try_parse_from([
6002            "openvmm",
6003            "--uefi",
6004            "firmware=new.fd",
6005            "--uefi-firmware",
6006            "legacy.fd",
6007        ])
6008        .unwrap();
6009        assert!(options.effective_uefi().is_err());
6010    }
6011
6012    #[test]
6013    fn test_guest_power_action_flags() {
6014        // Defaults preserve the historical behavior: reset and watchdog reboot,
6015        // power-off and crash keep the stopped VM.
6016        let opt = Options::try_parse_from(["openvmm"]).unwrap();
6017        assert_eq!(opt.guest_reset_action, GuestPowerAction::Reset);
6018        assert_eq!(opt.guest_shutdown_action, GuestPowerAction::Halt);
6019        assert_eq!(opt.guest_crash_action, GuestPowerAction::Halt);
6020        assert_eq!(opt.guest_watchdog_action, GuestPowerAction::Reset);
6021        // The CLI defaults must match the shared GuestPowerActions::default() the
6022        // ttrpc server uses, so the two launch paths never drift.
6023        assert_eq!(
6024            crate::vm_controller::GuestPowerActions {
6025                shutdown: opt.guest_shutdown_action,
6026                reset: opt.guest_reset_action,
6027                crash: opt.guest_crash_action,
6028                watchdog: opt.guest_watchdog_action,
6029            },
6030            crate::vm_controller::GuestPowerActions::default(),
6031        );
6032
6033        let opt = Options::try_parse_from([
6034            "openvmm",
6035            "--guest-watchdog",
6036            "--guest-reset-action",
6037            "exit",
6038            "--guest-shutdown-action",
6039            "exit:5",
6040            "--guest-crash-action",
6041            "reset",
6042            "--guest-watchdog-action",
6043            "halt",
6044        ])
6045        .unwrap();
6046        // A bare `exit` is status 0; `exit:5` carries the code through.
6047        assert_eq!(opt.guest_reset_action, GuestPowerAction::Exit(0));
6048        assert_eq!(opt.guest_shutdown_action, GuestPowerAction::Exit(5));
6049        assert_eq!(opt.guest_crash_action, GuestPowerAction::Reset);
6050        assert_eq!(opt.guest_watchdog_action, GuestPowerAction::Halt);
6051
6052        // Malformed and out-of-range exit codes are rejected (status is 0-255).
6053        assert!(Options::try_parse_from(["openvmm", "--guest-reset-action", "exit:nope"]).is_err());
6054        assert!(Options::try_parse_from(["openvmm", "--guest-reset-action", "exit:300"]).is_err());
6055        assert!(Options::try_parse_from(["openvmm", "--guest-reset-action", "exit:-1"]).is_err());
6056
6057        // --guest-watchdog-action requires the watchdog device (--guest-watchdog).
6058        assert!(Options::try_parse_from(["openvmm", "--guest-watchdog-action", "halt"]).is_err());
6059    }
6060
6061    #[cfg(target_os = "linux")]
6062    #[test]
6063    fn test_vfio_device_cli_parse() {
6064        use vfio_assigned_device_resources::BarAddressConfig;
6065
6066        // Required keys only.
6067        let v = VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0").unwrap();
6068        assert_eq!(v.pci_id, "0000:01:00.0");
6069        assert_eq!(v.port_name, "rp0");
6070        assert_eq!(v.iommu, None);
6071
6072        // With optional iommu= key. Keys may appear in any order.
6073        let v = VfioDeviceCli::from_str("port=rp1,iommu=iommu0,host=0000:02:00.0").unwrap();
6074        assert_eq!(v.pci_id, "0000:02:00.0");
6075        assert_eq!(v.port_name, "rp1");
6076        assert_eq!(v.iommu.as_deref(), Some("iommu0"));
6077
6078        let v = VfioDeviceCli::from_str(
6079            "host=0000:03:00.0,port=rp2,bar0=host,bar2=0x80000000,bar4=0x110000000000",
6080        )
6081        .unwrap();
6082        assert_eq!(v.bar_addresses[0], BarAddressConfig::HostAssigned);
6083        assert_eq!(v.bar_addresses[1], BarAddressConfig::GuestAssigned);
6084        assert_eq!(v.bar_addresses[2], BarAddressConfig::Fixed(0x80000000));
6085        assert_eq!(v.bar_addresses[4], BarAddressConfig::Fixed(0x110000000000));
6086    }
6087
6088    #[cfg(target_os = "linux")]
6089    #[test]
6090    fn test_vfio_device_cli_errors() {
6091        // Missing required keys.
6092        assert!(VfioDeviceCli::from_str("port=rp0").is_err());
6093        assert!(VfioDeviceCli::from_str("host=0000:01:00.0").is_err());
6094
6095        // Unknown key.
6096        assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,foo=bar").is_err());
6097
6098        // Duplicate keys are rejected.
6099        assert!(VfioDeviceCli::from_str("host=0000:01:00.0,host=0000:02:00.0,port=rp0").is_err());
6100        assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,port=rp1").is_err());
6101        assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,iommu=a,iommu=b").is_err());
6102
6103        // Empty values are rejected.
6104        assert!(VfioDeviceCli::from_str("host=,port=rp0").is_err());
6105        assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=").is_err());
6106        assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,iommu=").is_err());
6107
6108        // Missing '=' separator.
6109        assert!(VfioDeviceCli::from_str("host").is_err());
6110        assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,iommu").is_err());
6111
6112        // Path-traversal characters in the host BDF are rejected.
6113        assert!(VfioDeviceCli::from_str("host=../../etc/passwd,port=rp0").is_err());
6114        assert!(VfioDeviceCli::from_str("host=foo/bar,port=rp0").is_err());
6115
6116        // Invalid and duplicate BAR configurations are rejected.
6117        assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,bar0=0").is_err());
6118        assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,bar0=0x0").is_err());
6119        assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,bar0=0xnope").is_err());
6120        assert!(VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,bar0=pt").is_err());
6121        assert!(
6122            VfioDeviceCli::from_str("host=0000:01:00.0,port=rp0,bar0=0x1000,bar0=host").is_err()
6123        );
6124    }
6125
6126    #[cfg(target_os = "linux")]
6127    #[test]
6128    fn test_iommu_cli_parse() {
6129        let c = IommuCli::from_str("id=iommu0").unwrap();
6130        assert_eq!(c.id, "iommu0");
6131
6132        // Wrong key.
6133        assert!(IommuCli::from_str("name=iommu0").is_err());
6134
6135        // Missing '=' separator.
6136        assert!(IommuCli::from_str("iommu0").is_err());
6137
6138        // Empty id.
6139        assert!(IommuCli::from_str("id=").is_err());
6140    }
6141
6142    #[cfg(target_os = "linux")]
6143    #[test]
6144    fn test_vhost_user_cli() {
6145        // type=blk: socket is positional; num_queues/queue_size optional.
6146        let v = VhostUserCli::from_str("/run/blk.sock,type=blk,num_queues=2").unwrap();
6147        assert_eq!(v.socket_path, "/run/blk.sock");
6148        assert!(matches!(
6149            v.device_type,
6150            VhostUserDeviceTypeCli::Blk {
6151                num_queues: Some(2),
6152                queue_size: None
6153            }
6154        ));
6155        assert_eq!(v.pcie_port, None);
6156
6157        // type=fs requires a tag.
6158        let v = VhostUserCli::from_str("/run/fs.sock,type=fs,tag=myfs,pcie_port=p0").unwrap();
6159        assert!(matches!(
6160            &v.device_type,
6161            VhostUserDeviceTypeCli::Fs { tag, .. } if tag == "myfs"
6162        ));
6163        assert_eq!(v.pcie_port.as_deref(), Some("p0"));
6164
6165        // device_id with a bracketed queue_sizes list.
6166        let v = VhostUserCli::from_str("/run/x.sock,device_id=9,queue_sizes=[16,32]").unwrap();
6167        assert!(matches!(
6168            &v.device_type,
6169            VhostUserDeviceTypeCli::Other { device_id: 9, queue_sizes } if *queue_sizes == vec![16, 32]
6170        ));
6171
6172        // Errors.
6173        assert!(VhostUserCli::from_str("/run/x.sock").is_err()); // neither type nor device_id
6174        assert!(VhostUserCli::from_str("/run/x.sock,type=blk,device_id=1").is_err()); // both
6175        assert!(VhostUserCli::from_str("/run/x.sock,type=fs").is_err()); // fs without tag
6176        assert!(VhostUserCli::from_str("/run/x.sock,type=zzz").is_err()); // unknown type
6177        assert!(VhostUserCli::from_str("/run/x.sock,type=blk,tag=t").is_err()); // tag on non-fs
6178        assert!(VhostUserCli::from_str("/run/x.sock,type=blk,queue_sizes=[1]").is_err()); // queue_sizes on non-device_id
6179        assert!(VhostUserCli::from_str("/run/x.sock,device_id=1,num_queues=2").is_err()); // num_queues on device_id
6180        assert!(VhostUserCli::from_str("/run/x.sock,device_id=1,queue_sizes=[]").is_err()); // empty list
6181        assert!(VhostUserCli::from_str("/run/x.sock,device_id=1").is_err()); // device_id without queue_sizes
6182    }
6183
6184    #[cfg(target_os = "linux")]
6185    #[test]
6186    fn test_vhost_vsock_cli() {
6187        let opt = Options::try_parse_from(["openvmm", "--virtio-vsock-vhost-cid", "3"]).unwrap();
6188        assert_eq!(opt.virtio_vsock_vhost_cid, Some(3));
6189
6190        let opt =
6191            Options::try_parse_from(["openvmm", "--virtio-vsock-path", "/tmp/vsock"]).unwrap();
6192        assert_eq!(opt.virtio_vsock_path.as_deref(), Some("/tmp/vsock"));
6193
6194        assert!(Options::try_parse_from(["openvmm", "--virtio-vsock-vhost-cid", "2"]).is_err());
6195        assert!(
6196            Options::try_parse_from([
6197                "openvmm",
6198                "--virtio-vsock-vhost-cid",
6199                "3",
6200                "--virtio-vsock-path",
6201                "/tmp/vsock",
6202            ])
6203            .is_err()
6204        );
6205    }
6206
6207    #[test]
6208    fn test_virtio_vsock_bus_cli() {
6209        let opt = Options::try_parse_from(["openvmm", "--virtio-vsock-bus", "mmio"]).unwrap();
6210        assert_eq!(opt.virtio_vsock_bus, Some(VirtioBusCli::Mmio));
6211
6212        let opt = Options::try_parse_from(["openvmm", "--virtio-vsock-bus", "pci"]).unwrap();
6213        assert_eq!(opt.virtio_vsock_bus, Some(VirtioBusCli::Pci));
6214
6215        let opt = Options::try_parse_from(["openvmm", "--virtio-vsock-bus", "pcie"]).unwrap();
6216        assert_eq!(
6217            opt.virtio_vsock_bus,
6218            Some(VirtioBusCli::Pcie("vsock".to_string()))
6219        );
6220
6221        let opt =
6222            Options::try_parse_from(["openvmm", "--virtio-vsock-bus", "pcie:custom"]).unwrap();
6223        assert_eq!(
6224            opt.virtio_vsock_bus,
6225            Some(VirtioBusCli::Pcie("custom".to_string()))
6226        );
6227
6228        let opt =
6229            Options::try_parse_from(["openvmm", "--virtio-vsock-bus", "PCIE:custom"]).unwrap();
6230        assert_eq!(
6231            opt.virtio_vsock_bus,
6232            Some(VirtioBusCli::Pcie("custom".to_string()))
6233        );
6234
6235        assert!(Options::try_parse_from(["openvmm", "--virtio-vsock-bus", "auto"]).is_err());
6236        assert!(Options::try_parse_from(["openvmm", "--virtio-vsock-bus", "vpci"]).is_err());
6237        assert!(Options::try_parse_from(["openvmm", "--virtio-vsock-bus", "pcie:"]).is_err());
6238        assert!(
6239            Options::try_parse_from(["openvmm", "--virtio-vsock-bus", "pcie:custom:extra"])
6240                .is_err()
6241        );
6242
6243        let opt =
6244            Options::try_parse_from(["openvmm", "--virtio-rng", "--virtio-rng-bus", "pcie:custom"])
6245                .unwrap();
6246        assert_eq!(opt.virtio_rng_bus, VirtioBusCli::Pcie("custom".to_string()));
6247    }
6248
6249    #[test]
6250    fn test_virtio_rtc_cli() {
6251        let opt = Options::try_parse_from(["openvmm"]).unwrap();
6252        assert!(opt.virtio_rtc.is_none());
6253
6254        let opt =
6255            Options::try_parse_from(["openvmm", "--virtio-rtc", "--processors", "2"]).unwrap();
6256        assert_eq!(
6257            opt.virtio_rtc,
6258            Some(VirtioRtcCli {
6259                bus: VirtioBusCli::Auto
6260            })
6261        );
6262
6263        let opt = Options::try_parse_from(["openvmm", "--virtio-rtc", "bus=pcie:rp0"]).unwrap();
6264        assert_eq!(
6265            opt.virtio_rtc,
6266            Some(VirtioRtcCli {
6267                bus: VirtioBusCli::Pcie("rp0".to_string())
6268            })
6269        );
6270
6271        for value in ["mmio", "bus", "bus=", "bus=pcie:", "bus=pcie:rp0,bus=mmio"] {
6272            assert!(Options::try_parse_from(["openvmm", "--virtio-rtc", value]).is_err());
6273        }
6274    }
6275
6276    #[test]
6277    fn test_nvme_controller_cli_pcie() {
6278        let c = NvmeControllerCli::from_str("id=nvme0,pcie_port=p0").unwrap();
6279        assert_eq!(c.id, "nvme0");
6280        assert_eq!(c.transport, NvmeControllerTransport::Pcie("p0".into()));
6281    }
6282
6283    #[test]
6284    fn test_nvme_controller_cli_vpci_no_guid() {
6285        let c = NvmeControllerCli::from_str("id=nvme1,vpci").unwrap();
6286        assert_eq!(c.id, "nvme1");
6287        assert!(matches!(c.transport, NvmeControllerTransport::Vpci(None)));
6288        assert!(NvmeControllerCli::from_str("id=nvme1,vpci=").is_err());
6289    }
6290
6291    #[test]
6292    fn test_nvme_controller_cli_vpci_with_guid() {
6293        let c = NvmeControllerCli::from_str("id=nvme2,vpci=008091f6-9688-497d-9091-af347dc9173c")
6294            .unwrap();
6295        assert_eq!(c.id, "nvme2");
6296        assert!(matches!(
6297            c.transport,
6298            NvmeControllerTransport::Vpci(Some(_))
6299        ));
6300    }
6301
6302    #[test]
6303    fn test_nvme_controller_cli_errors() {
6304        // Missing id.
6305        assert!(NvmeControllerCli::from_str("pcie_port=p0").is_err());
6306        // Missing transport.
6307        assert!(NvmeControllerCli::from_str("id=nvme0").is_err());
6308        // Both transports.
6309        assert!(NvmeControllerCli::from_str("id=nvme0,pcie_port=p0,vpci").is_err());
6310        // Unknown option.
6311        assert!(NvmeControllerCli::from_str("id=nvme0,pcie_port=p0,foo=bar").is_err());
6312        // Empty id.
6313        assert!(NvmeControllerCli::from_str("id=,pcie_port=p0").is_err());
6314        // Empty pcie_port.
6315        assert!(NvmeControllerCli::from_str("id=nvme0,pcie_port=").is_err());
6316        // Invalid GUID.
6317        let err = NvmeControllerCli::from_str("id=nvme0,vpci=not-a-guid").unwrap_err();
6318        assert!(err.to_string().contains("invalid value for option 'vpci'"));
6319    }
6320
6321    #[test]
6322    fn test_disk_cli_controller() {
6323        let d = DiskCli::from_str("file:disk.vhd,on=nvme0").unwrap();
6324        assert_eq!(d.controller.as_deref(), Some("nvme0"));
6325        assert_eq!(d.nsid, None);
6326    }
6327
6328    #[test]
6329    fn test_disk_cli_serial() {
6330        let d = DiskCli::from_str("file:disk.vhd,serial=DATA-DISK").unwrap();
6331        assert_eq!(d.serial.as_deref(), Some("DATA-DISK"));
6332        assert!(DiskCli::from_str("file:disk.vhd,serial=").is_err());
6333        assert!(DiskCli::from_str("file:disk.vhd,serial=DATA[1]").is_err());
6334        assert!(DiskCli::from_str("file:disk.vhd,serial=DATA,DISK").is_err());
6335        assert!(DiskCli::from_str("file:disk.vhd,serial=DATA\u{1f}DISK").is_err());
6336        assert!(DiskCli::from_str("file:disk.vhd,serial=non-ascii-\u{e9}").is_err());
6337    }
6338
6339    #[test]
6340    fn test_disk_cli_controller_with_nsid() {
6341        let d = DiskCli::from_str("file:disk.vhd,on=nvme0,nsid=3").unwrap();
6342        assert_eq!(d.controller.as_deref(), Some("nvme0"));
6343        assert_eq!(d.nsid, Some(3));
6344    }
6345
6346    #[test]
6347    fn test_disk_cli_controller_errors() {
6348        // nsid without on.
6349        assert!(DiskCli::from_str("file:disk.vhd,nsid=1").is_err());
6350        // lun without on.
6351        assert!(DiskCli::from_str("file:disk.vhd,lun=0").is_err());
6352        // on with pcie_port.
6353        assert!(DiskCli::from_str("file:disk.vhd,on=nvme0,pcie_port=p0").is_err());
6354        // Empty controller name.
6355        assert!(DiskCli::from_str("file:disk.vhd,on=").is_err());
6356        // Invalid nsid.
6357        assert!(DiskCli::from_str("file:disk.vhd,on=nvme0,nsid=abc").is_err());
6358        // nsid and lun together.
6359        assert!(DiskCli::from_str("file:disk.vhd,on=c,nsid=1,lun=0").is_err());
6360    }
6361
6362    #[test]
6363    fn test_disk_cli_controller_with_lun() {
6364        let d = DiskCli::from_str("file:disk.vhd,on=scsi0,lun=3").unwrap();
6365        assert_eq!(d.controller.as_deref(), Some("scsi0"));
6366        assert_eq!(d.lun, Some(3));
6367        assert_eq!(d.nsid, None);
6368    }
6369
6370    #[test]
6371    fn test_scsi_controller_cli() {
6372        let c = ScsiControllerCli::from_str("id=scsi0").unwrap();
6373        assert_eq!(c.id, "scsi0");
6374        assert_eq!(c.sub_channels, 0);
6375    }
6376
6377    #[test]
6378    fn test_scsi_controller_cli_with_sub_channels() {
6379        let c = ScsiControllerCli::from_str("id=scsi1,sub_channels=4").unwrap();
6380        assert_eq!(c.id, "scsi1");
6381        assert_eq!(c.sub_channels, 4);
6382    }
6383
6384    #[test]
6385    fn test_scsi_controller_cli_errors() {
6386        // Missing id.
6387        assert!(ScsiControllerCli::from_str("sub_channels=4").is_err());
6388        // Empty id.
6389        assert!(ScsiControllerCli::from_str("id=").is_err());
6390        // Unknown option.
6391        assert!(ScsiControllerCli::from_str("id=scsi0,foo=bar").is_err());
6392        // Invalid sub_channels.
6393        assert!(ScsiControllerCli::from_str("id=scsi0,sub_channels=abc").is_err());
6394    }
6395
6396    #[test]
6397    fn test_disk_cli_relay() {
6398        let d = DiskCli::from_str("file:disk.vhd,on=src,relay=tgt").unwrap();
6399        assert_eq!(d.relay.as_ref().unwrap().0, "tgt");
6400        assert_eq!(d.relay.as_ref().unwrap().1, None);
6401    }
6402
6403    #[test]
6404    fn test_disk_cli_relay_with_location() {
6405        let d = DiskCli::from_str("file:disk.vhd,on=src,relay=tgt:3").unwrap();
6406        assert_eq!(d.relay.as_ref().unwrap().0, "tgt");
6407        assert_eq!(d.relay.as_ref().unwrap().1, Some(3));
6408    }
6409
6410    #[test]
6411    fn test_disk_cli_relay_errors() {
6412        // relay without on.
6413        assert!(DiskCli::from_str("file:disk.vhd,relay=tgt").is_err());
6414        // relay with uh.
6415        assert!(DiskCli::from_str("file:disk.vhd,on=src,relay=tgt,uh").is_err());
6416        // relay with invalid location.
6417        assert!(DiskCli::from_str("file:disk.vhd,on=src,relay=tgt:abc").is_err());
6418        // empty relay.
6419        assert!(DiskCli::from_str("file:disk.vhd,on=src,relay=").is_err());
6420    }
6421
6422    #[test]
6423    fn test_nvme_controller_cli_vtl2() {
6424        let c = NvmeControllerCli::from_str("id=nvme0,vpci,vtl2").unwrap();
6425        assert_eq!(c.vtl, DeviceVtl::Vtl2);
6426    }
6427
6428    #[test]
6429    fn test_scsi_controller_cli_vtl2() {
6430        let c = ScsiControllerCli::from_str("id=scsi0,vtl2").unwrap();
6431        assert_eq!(c.vtl, DeviceVtl::Vtl2);
6432    }
6433
6434    #[test]
6435    fn test_openhcl_controller_cli() {
6436        let c = OpenhclControllerCli::from_str("id=vtl0-scsi,type=scsi").unwrap();
6437        assert_eq!(c.id, "vtl0-scsi");
6438        assert_eq!(c.controller_type, OpenhclControllerType::Scsi);
6439        assert_eq!(c.guid, None);
6440    }
6441
6442    #[test]
6443    fn test_openhcl_controller_cli_nvme_with_guid() {
6444        let c = OpenhclControllerCli::from_str(
6445            "id=vtl0-nvme,type=nvme,guid=09a59b81-2bf6-4164-81d7-3a0dc977ba65",
6446        )
6447        .unwrap();
6448        assert_eq!(c.controller_type, OpenhclControllerType::Nvme);
6449        assert!(c.guid.is_some());
6450    }
6451
6452    #[test]
6453    fn test_openhcl_controller_cli_errors() {
6454        // Missing id.
6455        assert!(OpenhclControllerCli::from_str("type=scsi").is_err());
6456        // Missing type.
6457        assert!(OpenhclControllerCli::from_str("id=foo").is_err());
6458        // Invalid type.
6459        assert!(OpenhclControllerCli::from_str("id=foo,type=ide").is_err());
6460        // Invalid guid.
6461        assert!(OpenhclControllerCli::from_str("id=foo,type=scsi,guid=bad").is_err());
6462    }
6463
6464    #[test]
6465    fn test_parse_vp_list() {
6466        use vmm_cli::BracketRangeList;
6467
6468        let parse = |s: &str| {
6469            s.parse::<BracketRangeList>()
6470                .and_then(|v| v.expand_below(1024))
6471        };
6472
6473        // Individual indices.
6474        assert_eq!(parse("[0,1,2,3]").unwrap(), vec![0, 1, 2, 3]);
6475
6476        // Single index.
6477        assert_eq!(parse("[5]").unwrap(), vec![5]);
6478
6479        // Dash range.
6480        assert_eq!(parse("[0-3]").unwrap(), vec![0, 1, 2, 3]);
6481
6482        // Mixed indices and ranges.
6483        assert_eq!(parse("[0,1,4-6,10]").unwrap(), vec![0, 1, 4, 5, 6, 10]);
6484
6485        // Whitespace tolerance.
6486        assert_eq!(parse("[0, 1, 2-4]").unwrap(), vec![0, 1, 2, 3, 4]);
6487
6488        // Missing brackets.
6489        assert!(parse("0,1,2").is_err());
6490        assert!(parse("0-3").is_err());
6491
6492        // Inverted range.
6493        assert!(parse("[3-0]").is_err());
6494
6495        // Non-numeric.
6496        assert!(parse("[a,b]").is_err());
6497    }
6498
6499    #[test]
6500    fn test_parse_numa_node() {
6501        use super::parse_numa_node;
6502
6503        // Basic node with size only.
6504        let n = parse_numa_node("size=2G").unwrap();
6505        assert_eq!(
6506            n.memory.size,
6507            Some(vmm_cli::MemorySize(2 * 1024 * 1024 * 1024))
6508        );
6509        assert!(n.vps.is_none());
6510        assert!(n.host_numa_node.is_none());
6511
6512        // Node with bracket VP list.
6513        let n = parse_numa_node("size=1G,vps=[0,1,2,3]").unwrap();
6514        assert_eq!(n.vps.unwrap().expand_below(1024).unwrap(), [0, 1, 2, 3]);
6515
6516        // Node with VP range in brackets.
6517        let n = parse_numa_node("size=1G,vps=[0-3]").unwrap();
6518        assert_eq!(n.vps.unwrap().expand_below(1024).unwrap(), [0, 1, 2, 3]);
6519
6520        // Node with host_numa_node.
6521        let n = parse_numa_node("size=1G,host_numa_node=1").unwrap();
6522        assert_eq!(n.host_numa_node, Some(1));
6523
6524        // All options together.
6525        let n = parse_numa_node("size=1G,vps=[0,1],host_numa_node=0,hugepages=on").unwrap();
6526        assert_eq!(n.vps.unwrap().expand_below(1024).unwrap(), [0, 1]);
6527        assert_eq!(n.host_numa_node, Some(0));
6528        assert!(n.memory.hugepages);
6529
6530        // Missing size.
6531        assert!(parse_numa_node("vps=[0,1]").is_err());
6532
6533        // Bare vps without brackets.
6534        assert!(parse_numa_node("size=1G,vps=0,1").is_err());
6535
6536        // Duplicate vps.
6537        assert!(parse_numa_node("size=1G,vps=[0],vps=[1]").is_err());
6538
6539        // `file` is rejected for NUMA nodes.
6540        assert!(parse_numa_node("size=1G,file=/tmp/x").is_err());
6541
6542        // Empty vps=[] for memory-only node.
6543        let n = parse_numa_node("size=1G,vps=[]").unwrap();
6544        assert!(n.vps.unwrap().0.is_empty());
6545    }
6546
6547    #[test]
6548    fn test_parse_numa_distance() {
6549        use super::parse_numa_distance;
6550
6551        let d = parse_numa_distance("0:1:20").unwrap();
6552        assert_eq!(d.src, 0);
6553        assert_eq!(d.dst, 1);
6554        assert_eq!(d.distance, 20);
6555
6556        // Self-distance.
6557        let d = parse_numa_distance("0:0:10").unwrap();
6558        assert_eq!(d.distance, 10);
6559
6560        // Distance below minimum.
6561        assert!(parse_numa_distance("0:1:5").is_err());
6562
6563        // Wrong format.
6564        assert!(parse_numa_distance("0:1").is_err());
6565        assert!(parse_numa_distance("0:1:20:extra").is_err());
6566    }
6567
6568    #[cfg(guest_arch = "aarch64")]
6569    #[test]
6570    fn test_smmu_cli_from_str() {
6571        // Minimal: only rc=, oas defaults to auto, accel off.
6572        let s = SmmuCli::from_str("rc=pcie0").unwrap();
6573        assert_eq!(s.rc_name, "pcie0");
6574        assert!(!s.accel);
6575        assert!(matches!(s.oas, SmmuOasCli::Auto));
6576
6577        // accel flag.
6578        let s = SmmuCli::from_str("rc=pcie0,accel").unwrap();
6579        assert_eq!(s.rc_name, "pcie0");
6580        assert!(s.accel);
6581        assert!(matches!(s.oas, SmmuOasCli::Auto));
6582
6583        // Explicit oas=auto.
6584        let s = SmmuCli::from_str("rc=pcie0,oas=auto").unwrap();
6585        assert!(matches!(s.oas, SmmuOasCli::Auto));
6586
6587        // Fixed oas.
6588        let s = SmmuCli::from_str("rc=pcie0,oas=52").unwrap();
6589        assert!(matches!(s.oas, SmmuOasCli::Fixed(52)));
6590
6591        // All keys/flags together, order independent.
6592        let s = SmmuCli::from_str("oas=48,accel,rc=pcie1").unwrap();
6593        assert_eq!(s.rc_name, "pcie1");
6594        assert!(s.accel);
6595        assert!(matches!(s.oas, SmmuOasCli::Fixed(48)));
6596
6597        // Missing required rc=.
6598        assert!(SmmuCli::from_str("accel").is_err());
6599        assert!(SmmuCli::from_str("oas=52").is_err());
6600
6601        // Empty rc= value.
6602        assert!(SmmuCli::from_str("rc=").is_err());
6603
6604        // Duplicate rc= key.
6605        assert!(SmmuCli::from_str("rc=pcie0,rc=pcie1").is_err());
6606
6607        // Non-numeric oas value.
6608        assert!(SmmuCli::from_str("rc=pcie0,oas=big").is_err());
6609
6610        // Unknown key.
6611        assert!(SmmuCli::from_str("rc=pcie0,foo=bar").is_err());
6612
6613        // Unknown flag.
6614        assert!(SmmuCli::from_str("rc=pcie0,turbo").is_err());
6615    }
6616}