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