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