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