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 hvlite_defs::config::DEFAULT_PCAT_BOOT_ORDER;
25use hvlite_defs::config::DeviceVtl;
26use hvlite_defs::config::Hypervisor;
27use hvlite_defs::config::PcatBootDevice;
28use hvlite_defs::config::Vtl2BaseAddressType;
29use hvlite_defs::config::X2ApicConfig;
30use std::ffi::OsString;
31use std::net::SocketAddr;
32use std::path::PathBuf;
33use std::str::FromStr;
34use thiserror::Error;
35
36/// OpenVMM virtual machine monitor.
37///
38/// This is not yet a stable interface and may change radically between
39/// versions.
40#[derive(Parser)]
41pub struct Options {
42    /// processor count
43    #[clap(short = 'p', long, value_name = "COUNT", default_value = "1")]
44    pub processors: u32,
45
46    /// guest RAM size
47    #[clap(
48        short = 'm',
49        long,
50        value_name = "SIZE",
51        default_value = "1GB",
52        value_parser = parse_memory
53    )]
54    pub memory: u64,
55
56    /// use shared memory segment
57    #[clap(short = 'M', long)]
58    pub shared_memory: bool,
59
60    /// prefetch guest RAM
61    #[clap(long)]
62    pub prefetch: bool,
63
64    /// start in paused state
65    #[clap(short = 'P', long)]
66    pub paused: bool,
67
68    /// kernel image (when using linux direct boot)
69    #[clap(short = 'k', long, value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_LINUX_DIRECT_KERNEL"))]
70    pub kernel: OptionalPathBuf,
71
72    /// initrd image (when using linux direct boot)
73    #[clap(short = 'r', long, value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_LINUX_DIRECT_INITRD"))]
74    pub initrd: OptionalPathBuf,
75
76    /// extra kernel command line args
77    #[clap(short = 'c', long, value_name = "STRING")]
78    pub cmdline: Vec<String>,
79
80    /// enable HV#1 capabilities
81    #[clap(long)]
82    pub hv: bool,
83
84    /// enable vtl2 - only supported in WHP and simulated without hypervisor support currently
85    ///
86    /// Currently implies --get.
87    #[clap(long, requires("hv"))]
88    pub vtl2: bool,
89
90    /// Add GET and related devices for using the OpenHCL paravisor to the
91    /// highest enabled VTL.
92    #[clap(long, requires("hv"))]
93    pub get: bool,
94
95    /// Disable GET and related devices for using the OpenHCL paravisor, even
96    /// when --vtl2 is passed.
97    #[clap(long, conflicts_with("get"))]
98    pub no_get: bool,
99
100    /// disable the VTL0 alias map presented to VTL2 by default
101    #[clap(long, requires("vtl2"))]
102    pub no_alias_map: bool,
103
104    /// enable isolation emulation
105    #[clap(long, requires("vtl2"))]
106    pub isolation: Option<IsolationCli>,
107
108    /// the hybrid vsock listener path
109    #[clap(long, value_name = "PATH")]
110    pub vsock_path: Option<String>,
111
112    /// the VTL2 hybrid vsock listener path
113    #[clap(long, value_name = "PATH", requires("vtl2"))]
114    pub vtl2_vsock_path: Option<String>,
115
116    /// the late map vtl0 ram access policy when vtl2 is enabled
117    #[clap(long, requires("vtl2"), default_value = "halt")]
118    pub late_map_vtl0_policy: Vtl0LateMapPolicyCli,
119
120    /// disable in-hypervisor enlightenment implementation (where possible)
121    #[clap(long)]
122    pub no_enlightenments: bool,
123
124    /// disable the in-hypervisor APIC and use the user-mode one (where possible)
125    #[clap(long)]
126    pub user_mode_apic: bool,
127
128    /// attach a disk (can be passed multiple times)
129    #[clap(long_help = r#"
130e.g: --disk memdiff:file:/path/to/disk.vhd
131
132syntax: <path> | kind:<arg>[,flag,opt=arg,...]
133
134valid disk kinds:
135    `mem:<len>`                    memory backed disk
136        <len>: length of ramdisk, e.g.: `1G`
137    `memdiff:<disk>`               memory backed diff disk
138        <disk>: lower disk, e.g.: `file:base.img`
139    `file:<path>`                  file-backed disk
140        <path>: path to file
141
142flags:
143    `ro`                           open disk as read-only
144    `dvd`                          specifies that device is cd/dvd and it is read_only
145    `vtl2`                         assign this disk to VTL2
146    `uh`                           relay this disk to VTL0 through SCSI-to-OpenHCL (show to VTL0 as SCSI)
147    `uh-nvme`                      relay this disk to VTL0 through NVMe-to-OpenHCL (show to VTL0 as SCSI)
148"#)]
149    #[clap(long, value_name = "FILE")]
150    pub disk: Vec<DiskCli>,
151
152    /// attach a disk via an NVMe controller
153    #[clap(long_help = r#"
154e.g: --nvme memdiff:file:/path/to/disk.vhd
155
156syntax: <path> | kind:<arg>[,flag,opt=arg,...]
157
158valid disk kinds:
159    `mem:<len>`                    memory backed disk
160        <len>: length of ramdisk, e.g.: `1G`
161    `memdiff:<disk>`               memory backed diff disk
162        <disk>: lower disk, e.g.: `file:base.img`
163    `file:<path>`                  file-backed disk
164        <path>: path to file
165
166flags:
167    `ro`                           open disk as read-only
168    `vtl2`                         assign this disk to VTL2
169    `uh`                           relay this disk to VTL0 through SCSI-to-OpenHCL (show to VTL0 as NVMe)
170    `uh-nvme`                      relay this disk to VTL0 through NVMe-to-OpenHCL (show to VTL0 as NVMe)
171"#)]
172    #[clap(long)]
173    pub nvme: Vec<DiskCli>,
174
175    /// number of sub-channels for the SCSI controller
176    #[clap(long, value_name = "COUNT", default_value = "0")]
177    pub scsi_sub_channels: u16,
178
179    /// expose a virtual NIC
180    #[clap(long)]
181    pub nic: bool,
182
183    /// expose a virtual NIC with the given backend (consomme | dio | tap | none)
184    ///
185    /// Prefix with `uh:` to add this NIC via Mana emulation through OpenHCL,
186    /// or `vtl2:` to assign this NIC to VTL2.
187    #[clap(long)]
188    pub net: Vec<NicConfigCli>,
189
190    /// expose a virtual NIC using the Windows kernel-mode vmswitch.
191    ///
192    /// Specify the switch ID or "default" for the default switch.
193    #[clap(long, value_name = "SWITCH_ID")]
194    pub kernel_vmnic: Vec<String>,
195
196    /// expose a graphics device
197    #[clap(long)]
198    pub gfx: bool,
199
200    /// support a graphics device in vtl2
201    #[clap(long, requires("vtl2"), conflicts_with("gfx"))]
202    pub vtl2_gfx: bool,
203
204    /// listen for vnc connections. implied by gfx.
205    #[clap(long)]
206    pub vnc: bool,
207
208    /// VNC port number
209    #[clap(long, value_name = "PORT", default_value = "5900")]
210    pub vnc_port: u16,
211
212    /// set the APIC ID offset, for testing APIC IDs that don't match VP index
213    #[cfg(guest_arch = "x86_64")]
214    #[clap(long, default_value_t)]
215    pub apic_id_offset: u32,
216
217    /// the maximum number of VPs per socket
218    #[clap(long)]
219    pub vps_per_socket: Option<u32>,
220
221    /// enable or disable SMT (hyperthreading) (auto | force | off)
222    #[clap(long, default_value = "auto")]
223    pub smt: SmtConfigCli,
224
225    /// configure x2apic (auto | supported | off | on)
226    #[cfg(guest_arch = "x86_64")]
227    #[clap(long, default_value = "auto", value_parser = parse_x2apic)]
228    pub x2apic: X2ApicConfig,
229
230    /// use virtio console
231    #[clap(long)]
232    pub virtio_console: bool,
233
234    /// use virtio console enumerated via VPCI
235    #[clap(long, conflicts_with("virtio_console"))]
236    pub virtio_console_pci: bool,
237
238    /// COM1 binding (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>][,name=<windowtitle>] | none)
239    #[clap(long, value_name = "SERIAL")]
240    pub com1: Option<SerialConfigCli>,
241
242    /// COM2 binding (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>][,name=<windowtitle>] | none)
243    #[clap(long, value_name = "SERIAL")]
244    pub com2: Option<SerialConfigCli>,
245
246    /// COM3 binding (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>][,name=<windowtitle>] | none)
247    #[clap(long, value_name = "SERIAL")]
248    pub com3: Option<SerialConfigCli>,
249
250    /// COM4 binding (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>][,name=<windowtitle>] | none)
251    #[clap(long, value_name = "SERIAL")]
252    pub com4: Option<SerialConfigCli>,
253
254    /// virtio serial binding (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>][,name=<windowtitle>] | none)
255    #[clap(long, value_name = "SERIAL")]
256    pub virtio_serial: Option<SerialConfigCli>,
257
258    /// vmbus com1 serial binding (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>][,name=<windowtitle>] | none)
259    #[structopt(long, value_name = "SERIAL")]
260    pub vmbus_com1_serial: Option<SerialConfigCli>,
261
262    /// vmbus com2 serial binding (console | stderr | listen=\<path\> | file=\<path\> (overwrites) | listen=tcp:\<ip\>:\<port\> | term[=\<program\>][,name=<windowtitle>] | none)
263    #[structopt(long, value_name = "SERIAL")]
264    pub vmbus_com2_serial: Option<SerialConfigCli>,
265
266    /// 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))
267    #[clap(long, value_name = "SERIAL")]
268    pub debugcon: Option<DebugconSerialConfigCli>,
269
270    /// boot UEFI firmware
271    #[clap(long, short = 'e')]
272    pub uefi: bool,
273
274    /// UEFI firmware file
275    #[clap(long, requires("uefi"), conflicts_with("igvm"), value_name = "FILE", default_value = default_value_from_arch_env("OPENVMM_UEFI_FIRMWARE"))]
276    pub uefi_firmware: OptionalPathBuf,
277
278    /// enable UEFI debugging on COM1
279    #[clap(long, requires("uefi"))]
280    pub uefi_debug: bool,
281
282    /// enable memory protections in UEFI
283    #[clap(long, requires("uefi"))]
284    pub uefi_enable_memory_protections: bool,
285
286    /// set PCAT boot order as comma-separated string of boot device types
287    /// (e.g: floppy,hdd,optical,net).
288    ///
289    /// If less than 4 entries are added, entries are added according to their
290    /// default boot order (optical,hdd,net,floppy)
291    ///
292    /// e.g: passing "floppy,optical" will result in a boot order equivalent to
293    /// "floppy,optical,hdd,net".
294    ///
295    /// Passing duplicate types is an error.
296    #[clap(long, requires("pcat"))]
297    pub pcat_boot_order: Option<PcatBootOrderCli>,
298
299    /// Boot with PCAT BIOS firmware and piix4 devices
300    #[clap(long, conflicts_with("uefi"))]
301    pub pcat: bool,
302
303    /// PCAT firmware file
304    #[clap(long, requires("pcat"), value_name = "FILE")]
305    pub pcat_firmware: Option<PathBuf>,
306
307    /// boot IGVM file
308    #[clap(long, conflicts_with("kernel"), value_name = "FILE")]
309    pub igvm: Option<PathBuf>,
310
311    /// specify igvm vtl2 relocation type
312    /// (absolute=\<addr\>, disable, auto=\<filesize,or memory size\>, vtl2=\<filesize,or memory size\>,)
313    #[clap(long, requires("igvm"), default_value = "auto=filesize", value_parser = parse_vtl2_relocation)]
314    pub igvm_vtl2_relocation_type: Vtl2BaseAddressType,
315
316    /// add a virtio_9p device (e.g. myfs,C:\)
317    #[clap(long, value_name = "tag,root_path")]
318    pub virtio_9p: Vec<FsArgs>,
319
320    /// output debug info from the 9p server
321    #[clap(long)]
322    pub virtio_9p_debug: bool,
323
324    /// add a virtio_fs device (e.g. myfs,C:\,uid=1000,gid=2000)
325    #[clap(long, value_name = "tag,root_path,[options]")]
326    pub virtio_fs: Vec<FsArgsWithOptions>,
327
328    /// add a virtio_fs device for sharing memory (e.g. myfs,\SectionDirectoryPath)
329    #[clap(long, value_name = "tag,root_path")]
330    pub virtio_fs_shmem: Vec<FsArgs>,
331
332    /// add a virtio_fs device under either the PCI or MMIO bus, or whatever the hypervisor supports (pci | mmio | auto)
333    #[clap(long, value_name = "BUS", default_value = "auto")]
334    pub virtio_fs_bus: VirtioBusCli,
335
336    /// virtio PMEM device
337    #[clap(long, value_name = "PATH")]
338    pub virtio_pmem: Option<String>,
339
340    /// expose a virtio network with the given backend (dio | vmnic | tap |
341    /// none)
342    ///
343    /// Prefix with `uh:` to add this NIC via Mana emulation through OpenHCL,
344    /// or `vtl2:` to assign this NIC to VTL2.
345    #[clap(long)]
346    pub virtio_net: Vec<NicConfigCli>,
347
348    /// send log output from the worker process to a file instead of stderr. the file will be overwritten.
349    #[clap(long, value_name = "PATH")]
350    pub log_file: Option<PathBuf>,
351
352    /// run as a ttrpc server on the specified Unix socket
353    #[clap(long, value_name = "SOCKETPATH")]
354    pub ttrpc: Option<PathBuf>,
355
356    /// run as a grpc server on the specified Unix socket
357    #[clap(long, value_name = "SOCKETPATH", conflicts_with("ttrpc"))]
358    pub grpc: Option<PathBuf>,
359
360    /// do not launch child processes
361    #[clap(long)]
362    pub single_process: bool,
363
364    /// device to assign (can be passed multiple times)
365    #[cfg(windows)]
366    #[clap(long, value_name = "PATH")]
367    pub device: Vec<String>,
368
369    /// instead of showing the frontpage the VM will shutdown instead
370    #[clap(long, requires("uefi"))]
371    pub disable_frontpage: bool,
372
373    /// add a vtpm device
374    #[clap(long)]
375    pub tpm: bool,
376
377    /// the mesh worker host name.
378    ///
379    /// Used internally for debugging and diagnostics.
380    #[clap(long, default_value = "control", hide(true))]
381    #[expect(clippy::option_option)]
382    pub internal_worker: Option<Option<String>>,
383
384    /// redirect the VTL 0 vmbus control plane to a proxy in VTL 2.
385    #[clap(long, requires("vtl2"))]
386    pub vmbus_redirect: bool,
387
388    /// limit the maximum protocol version allowed by vmbus; used for testing purposes
389    #[clap(long, value_parser = vmbus_core::parse_vmbus_version)]
390    pub vmbus_max_version: Option<u32>,
391
392    /// The disk to use for the VMGS.
393    ///
394    /// If this is not provided, guest state will be stored in memory.
395    #[clap(long_help = r#"
396e.g: --vmgs memdiff:file:/path/to/file.vmgs
397
398syntax: <path> | kind:<arg>[,flag]
399
400valid disk kinds:
401    `mem:<len>`                     memory backed disk
402        <len>: length of ramdisk, e.g.: `1G` or `VMGS_DEFAULT`
403    `memdiff:<disk>[;create=<len>]` memory backed diff disk
404        <disk>: lower disk, e.g.: `file:base.img`
405    `file:<path>`                   file-backed disk
406        <path>: path to file
407
408flags:
409    `fmt`                           reprovision the VMGS before boot
410    `fmt-on-fail`                   reprovision the VMGS before boot if it is corrupted
411"#)]
412    #[clap(long)]
413    pub vmgs: Option<VmgsCli>,
414
415    /// Use GspById guest state encryption policy with a test seed
416    #[clap(long, requires("vmgs"))]
417    pub test_gsp_by_id: bool,
418
419    /// VGA firmware file
420    #[clap(long, requires("pcat"), value_name = "FILE")]
421    pub vga_firmware: Option<PathBuf>,
422
423    /// enable secure boot
424    #[clap(long)]
425    pub secure_boot: bool,
426
427    /// use secure boot template
428    #[clap(long)]
429    pub secure_boot_template: Option<SecureBootTemplateCli>,
430
431    /// custom uefi nvram json file
432    #[clap(long, value_name = "PATH")]
433    pub custom_uefi_json: Option<PathBuf>,
434
435    /// the path to a named pipe (Windows) or Unix socket (Linux) to relay to the connected
436    /// tty.
437    ///
438    /// This is a hidden argument used internally.
439    #[clap(long, hide(true))]
440    pub relay_console_path: Option<PathBuf>,
441
442    /// the title of the console window spawned from the relay console.
443    ///
444    /// This is a hidden argument used internally.
445    #[clap(long, hide(true))]
446    pub relay_console_title: Option<String>,
447
448    /// enable in-hypervisor gdb debugger
449    #[clap(long, value_name = "PORT")]
450    pub gdb: Option<u16>,
451
452    /// enable emulated MANA devices with the given network backend (see --net)
453    #[clap(long)]
454    pub mana: Vec<NicConfigCli>,
455
456    /// use a specific hypervisor interface
457    #[clap(long, value_parser = parse_hypervisor)]
458    pub hypervisor: Option<Hypervisor>,
459
460    /// (dev utility) boot linux using a custom (raw) DSDT table.
461    ///
462    /// This is a _very_ niche utility, and it's unlikely you'll need to use it.
463    ///
464    /// e.g: this flag helped bring up certain Hyper-V Generation 1 legacy
465    /// devices without needing to port the associated ACPI code into HvLite's
466    /// DSDT builder.
467    #[clap(long, value_name = "FILE", conflicts_with_all(&["uefi", "pcat", "igvm"]))]
468    pub custom_dsdt: Option<PathBuf>,
469
470    /// attach an ide drive (can be passed multiple times)
471    ///
472    /// Each ide controller has two channels. Each channel can have up to two
473    /// attachments.
474    ///
475    /// If the `s` flag is not passed then the drive will we be attached to the
476    /// primary ide channel if space is available. If two attachments have already
477    /// been added to the primary channel then the drive will be attached to the
478    /// secondary channel.
479    #[clap(long_help = r#"
480e.g: --ide memdiff:file:/path/to/disk.vhd
481
482syntax: <path> | kind:<arg>[,flag,opt=arg,...]
483
484valid disk kinds:
485    `mem:<len>`                    memory backed disk
486        <len>: length of ramdisk, e.g.: `1G`
487    `memdiff:<disk>`               memory backed diff disk
488        <disk>: lower disk, e.g.: `file:base.img`
489    `file:<path>`                  file-backed disk
490        <path>: path to file
491
492flags:
493    `ro`                           open disk as read-only
494    `s`                            attach drive to secondary ide channel
495    `dvd`                          specifies that device is cd/dvd and it is read_only
496"#)]
497    #[clap(long, value_name = "FILE", requires("pcat"))]
498    pub ide: Vec<IdeDiskCli>,
499
500    /// attach a floppy drive (should be able to be passed multiple times). VM must be generation 1 (no UEFI)
501    ///
502    #[clap(long_help = r#"
503e.g: --floppy memdiff:/path/to/disk.vfd,ro
504
505syntax: <path> | kind:<arg>[,flag,opt=arg,...]
506
507valid disk kinds:
508    `mem:<len>`                    memory backed disk
509        <len>: length of ramdisk, e.g.: `1G`
510    `memdiff:<disk>`               memory backed diff disk
511        <disk>: lower disk, e.g.: `file:base.img`
512    `file:<path>`                  file-backed disk
513        <path>: path to file
514
515flags:
516    `ro`                           open disk as read-only
517"#)]
518    #[clap(long, value_name = "FILE", requires("pcat"))]
519    pub floppy: Vec<FloppyDiskCli>,
520
521    /// enable guest watchdog device
522    #[clap(long)]
523    pub guest_watchdog: bool,
524
525    /// enable OpenHCL's guest crash dump device, targeting the specified path
526    #[clap(long)]
527    pub openhcl_dump_path: Option<PathBuf>,
528
529    /// halt the VM when the guest requests a reset, instead of resetting it
530    #[clap(long)]
531    pub halt_on_reset: bool,
532
533    /// write saved state .proto files to the specified path
534    #[clap(long)]
535    pub write_saved_state_proto: Option<PathBuf>,
536
537    /// specify the IMC hive file for booting Windows
538    #[clap(long)]
539    pub imc: Option<PathBuf>,
540
541    /// Expose MCR device
542    #[clap(long)]
543    pub mcr: bool, // TODO MCR: support closed source CLI flags
544
545    /// expose a battery device
546    #[clap(long)]
547    pub battery: bool,
548
549    /// set the uefi console mode
550    #[clap(long)]
551    pub uefi_console_mode: Option<UefiConsoleModeCli>,
552
553    /// Perform a default boot even if boot entries exist and fail
554    #[clap(long)]
555    pub default_boot_always_attempt: bool,
556
557    /// Attach a PCI Express root complex to the VM
558    #[clap(long_help = r#"
559e.g: --pcie-root-complex rc0,segment=0,start_bus=0,end_bus=255,low_mmio=4M,high_mmio=1G
560
561syntax: <name>[,opt=arg,...]
562
563options:
564    `segment=<value>`              configures the PCI Express segment, default 0
565    `start_bus=<value>`            lowest valid bus number, default 0
566    `end_bus=<value>`              highest valid bus number, default 255
567    `low_mmio=<size>`              low MMIO window size, default 4M
568    `high_mmio=<size>`             high MMIO window size, default 1G
569"#)]
570    #[clap(long, conflicts_with("pcat"))]
571    pub pcie_root_complex: Vec<PcieRootComplexCli>,
572
573    /// Attach a PCI Express root port to the VM
574    #[clap(long_help = r#"
575e.g: --pcie-root-port rc0:rc0rp0
576
577syntax: <root_complex_name>:<name>
578"#)]
579    #[clap(long, conflicts_with("pcat"))]
580    pub pcie_root_port: Vec<PcieRootPortCli>,
581}
582
583#[derive(Clone, Debug, PartialEq)]
584pub struct FsArgs {
585    pub tag: String,
586    pub path: String,
587}
588
589impl FromStr for FsArgs {
590    type Err = anyhow::Error;
591
592    fn from_str(s: &str) -> Result<Self, Self::Err> {
593        let mut s = s.split(',');
594        let (Some(tag), Some(path), None) = (s.next(), s.next(), s.next()) else {
595            anyhow::bail!("expected <tag>,<path>");
596        };
597        Ok(Self {
598            tag: tag.to_owned(),
599            path: path.to_owned(),
600        })
601    }
602}
603
604#[derive(Clone, Debug, PartialEq)]
605pub struct FsArgsWithOptions {
606    /// The file system tag.
607    pub tag: String,
608    /// The root path.
609    pub path: String,
610    /// The extra options, joined with ';'.
611    pub options: String,
612}
613
614impl FromStr for FsArgsWithOptions {
615    type Err = anyhow::Error;
616
617    fn from_str(s: &str) -> Result<Self, Self::Err> {
618        let mut s = s.split(',');
619        let (Some(tag), Some(path)) = (s.next(), s.next()) else {
620            anyhow::bail!("expected <tag>,<path>[,<options>]");
621        };
622        let options = s.collect::<Vec<_>>().join(";");
623        Ok(Self {
624            tag: tag.to_owned(),
625            path: path.to_owned(),
626            options,
627        })
628    }
629}
630
631#[derive(Copy, Clone, clap::ValueEnum)]
632pub enum VirtioBusCli {
633    Auto,
634    Mmio,
635    Pci,
636    Vpci,
637}
638
639#[derive(clap::ValueEnum, Clone, Copy)]
640pub enum SecureBootTemplateCli {
641    Windows,
642    UefiCa,
643}
644
645fn parse_memory(s: &str) -> anyhow::Result<u64> {
646    if s == "VMGS_DEFAULT" {
647        Ok(vmgs_format::VMGS_DEFAULT_CAPACITY)
648    } else {
649        || -> Option<u64> {
650            let mut b = s.as_bytes();
651            if s.ends_with('B') {
652                b = &b[..b.len() - 1]
653            }
654            if b.is_empty() {
655                return None;
656            }
657            let multi = match b[b.len() - 1] as char {
658                'T' => Some(1024 * 1024 * 1024 * 1024),
659                'G' => Some(1024 * 1024 * 1024),
660                'M' => Some(1024 * 1024),
661                'K' => Some(1024),
662                _ => None,
663            };
664            if multi.is_some() {
665                b = &b[..b.len() - 1]
666            }
667            let n: u64 = std::str::from_utf8(b).ok()?.parse().ok()?;
668            Some(n * multi.unwrap_or(1))
669        }()
670        .with_context(|| format!("invalid memory size '{0}'", s))
671    }
672}
673
674/// Parse a number from a string that could be prefixed with 0x to indicate hex.
675fn parse_number(s: &str) -> Result<u64, std::num::ParseIntError> {
676    match s.strip_prefix("0x") {
677        Some(rest) => u64::from_str_radix(rest, 16),
678        None => s.parse::<u64>(),
679    }
680}
681
682#[derive(Clone, Debug, PartialEq)]
683pub enum DiskCliKind {
684    // mem:<len>
685    Memory(u64),
686    // memdiff:<kind>
687    MemoryDiff(Box<DiskCliKind>),
688    // sql:<path>[;create=<len>]
689    Sqlite {
690        path: PathBuf,
691        create_with_len: Option<u64>,
692    },
693    // sqldiff:<path>[;create]:<kind>
694    SqliteDiff {
695        path: PathBuf,
696        create: bool,
697        disk: Box<DiskCliKind>,
698    },
699    // autocache:[key]:<kind>
700    AutoCacheSqlite {
701        cache_path: String,
702        key: Option<String>,
703        disk: Box<DiskCliKind>,
704    },
705    // prwrap:<kind>
706    PersistentReservationsWrapper(Box<DiskCliKind>),
707    // file:<path>[;create=<len>]
708    File {
709        path: PathBuf,
710        create_with_len: Option<u64>,
711    },
712    // blob:<type>:<url>
713    Blob {
714        kind: BlobKind,
715        url: String,
716    },
717    // crypt:<cipher>:<key_file>:<kind>
718    Crypt {
719        cipher: DiskCipher,
720        key_file: PathBuf,
721        disk: Box<DiskCliKind>,
722    },
723    // delay:<delay_ms>:<kind>
724    DelayDiskWrapper {
725        delay_ms: u64,
726        disk: Box<DiskCliKind>,
727    },
728}
729
730#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)]
731pub enum DiskCipher {
732    #[clap(name = "xts-aes-256")]
733    XtsAes256,
734}
735
736#[derive(Copy, Clone, Debug, PartialEq)]
737pub enum BlobKind {
738    Flat,
739    Vhd1,
740}
741
742fn parse_path_and_len(arg: &str) -> anyhow::Result<(PathBuf, Option<u64>)> {
743    Ok(match arg.split_once(';') {
744        Some((path, len)) => {
745            let Some(len) = len.strip_prefix("create=") else {
746                anyhow::bail!("invalid syntax after ';', expected 'create=<len>'")
747            };
748
749            let len = parse_memory(len)?;
750
751            (path.into(), Some(len))
752        }
753        None => (arg.into(), None),
754    })
755}
756
757impl FromStr for DiskCliKind {
758    type Err = anyhow::Error;
759
760    fn from_str(s: &str) -> anyhow::Result<Self> {
761        let disk = match s.split_once(':') {
762            // convenience support for passing bare paths as file disks
763            None => {
764                let (path, create_with_len) = parse_path_and_len(s)?;
765                DiskCliKind::File {
766                    path,
767                    create_with_len,
768                }
769            }
770            Some((kind, arg)) => match kind {
771                "mem" => DiskCliKind::Memory(parse_memory(arg)?),
772                "memdiff" => DiskCliKind::MemoryDiff(Box::new(arg.parse()?)),
773                "sql" => {
774                    let (path, create_with_len) = parse_path_and_len(arg)?;
775                    DiskCliKind::Sqlite {
776                        path,
777                        create_with_len,
778                    }
779                }
780                "sqldiff" => {
781                    let (path_and_opts, kind) =
782                        arg.split_once(':').context("expected path[;opts]:kind")?;
783                    let disk = Box::new(kind.parse()?);
784                    match path_and_opts.split_once(';') {
785                        Some((path, create)) => {
786                            if create != "create" {
787                                anyhow::bail!("invalid syntax after ';', expected 'create'")
788                            }
789                            DiskCliKind::SqliteDiff {
790                                path: path.into(),
791                                create: true,
792                                disk,
793                            }
794                        }
795                        None => DiskCliKind::SqliteDiff {
796                            path: path_and_opts.into(),
797                            create: false,
798                            disk,
799                        },
800                    }
801                }
802                "autocache" => {
803                    let (key, kind) = arg.split_once(':').context("expected [key]:kind")?;
804                    let cache_path = std::env::var("OPENVMM_AUTO_CACHE_PATH")
805                        .context("must set cache path via OPENVMM_AUTO_CACHE_PATH")?;
806                    DiskCliKind::AutoCacheSqlite {
807                        cache_path,
808                        key: (!key.is_empty()).then(|| key.to_string()),
809                        disk: Box::new(kind.parse()?),
810                    }
811                }
812                "prwrap" => DiskCliKind::PersistentReservationsWrapper(Box::new(arg.parse()?)),
813                "file" => {
814                    let (path, create_with_len) = parse_path_and_len(arg)?;
815                    DiskCliKind::File {
816                        path,
817                        create_with_len,
818                    }
819                }
820                "blob" => {
821                    let (blob_kind, url) = arg.split_once(':').context("expected kind:url")?;
822                    let blob_kind = match blob_kind {
823                        "flat" => BlobKind::Flat,
824                        "vhd1" => BlobKind::Vhd1,
825                        _ => anyhow::bail!("unknown blob kind {blob_kind}"),
826                    };
827                    DiskCliKind::Blob {
828                        kind: blob_kind,
829                        url: url.to_string(),
830                    }
831                }
832                "crypt" => {
833                    let (cipher, (key, kind)) = arg
834                        .split_once(':')
835                        .and_then(|(cipher, arg)| Some((cipher, arg.split_once(':')?)))
836                        .context("expected cipher:key_file:kind")?;
837                    DiskCliKind::Crypt {
838                        cipher: ValueEnum::from_str(cipher, false)
839                            .map_err(|err| anyhow::anyhow!("invalid cipher: {err}"))?,
840                        key_file: PathBuf::from(key),
841                        disk: Box::new(kind.parse()?),
842                    }
843                }
844                kind => {
845                    // here's a fun edge case: what if the user passes `--disk d:\path\to\disk.img`?
846                    //
847                    // in this case, we actually want to treat that leading `d:` as part of the
848                    // path, rather than as a disk with `kind == 'd'`
849                    let (path, create_with_len) = parse_path_and_len(s)?;
850                    if path.has_root() {
851                        DiskCliKind::File {
852                            path,
853                            create_with_len,
854                        }
855                    } else {
856                        anyhow::bail!("invalid disk kind {kind}");
857                    }
858                }
859            },
860        };
861        Ok(disk)
862    }
863}
864
865#[derive(Clone)]
866pub struct VmgsCli {
867    pub kind: DiskCliKind,
868    pub provision: ProvisionVmgs,
869}
870
871#[derive(Copy, Clone)]
872pub enum ProvisionVmgs {
873    OnEmpty,
874    OnFailure,
875    True,
876}
877
878impl FromStr for VmgsCli {
879    type Err = anyhow::Error;
880
881    fn from_str(s: &str) -> anyhow::Result<Self> {
882        let (kind, opt) = s
883            .split_once(',')
884            .map(|(k, o)| (k, Some(o)))
885            .unwrap_or((s, None));
886        let kind = kind.parse()?;
887
888        let provision = match opt {
889            None => ProvisionVmgs::OnEmpty,
890            Some("fmt-on-fail") => ProvisionVmgs::OnFailure,
891            Some("fmt") => ProvisionVmgs::True,
892            Some(opt) => anyhow::bail!("unknown option: '{opt}'"),
893        };
894
895        Ok(VmgsCli { kind, provision })
896    }
897}
898
899// <kind>[,ro]
900#[derive(Clone)]
901pub struct DiskCli {
902    pub vtl: DeviceVtl,
903    pub kind: DiskCliKind,
904    pub read_only: bool,
905    pub is_dvd: bool,
906    pub underhill: Option<UnderhillDiskSource>,
907}
908
909#[derive(Copy, Clone)]
910pub enum UnderhillDiskSource {
911    Scsi,
912    Nvme,
913}
914
915impl FromStr for DiskCli {
916    type Err = anyhow::Error;
917
918    fn from_str(s: &str) -> anyhow::Result<Self> {
919        let mut opts = s.split(',');
920        let kind = opts.next().unwrap().parse()?;
921
922        let mut read_only = false;
923        let mut is_dvd = false;
924        let mut underhill = None;
925        let mut vtl = DeviceVtl::Vtl0;
926        for opt in opts {
927            let mut s = opt.split('=');
928            let opt = s.next().unwrap();
929            match opt {
930                "ro" => read_only = true,
931                "dvd" => {
932                    is_dvd = true;
933                    read_only = true;
934                }
935                "vtl2" => {
936                    vtl = DeviceVtl::Vtl2;
937                }
938                "uh" => underhill = Some(UnderhillDiskSource::Scsi),
939                "uh-nvme" => underhill = Some(UnderhillDiskSource::Nvme),
940                opt => anyhow::bail!("unknown option: '{opt}'"),
941            }
942        }
943
944        if underhill.is_some() && vtl != DeviceVtl::Vtl0 {
945            anyhow::bail!("`uh` or `uh-nvme` is incompatible with `vtl2`");
946        }
947
948        Ok(DiskCli {
949            vtl,
950            kind,
951            read_only,
952            is_dvd,
953            underhill,
954        })
955    }
956}
957
958// <kind>[,ro,s]
959#[derive(Clone)]
960pub struct IdeDiskCli {
961    pub kind: DiskCliKind,
962    pub read_only: bool,
963    pub channel: Option<u8>,
964    pub device: Option<u8>,
965    pub is_dvd: bool,
966}
967
968impl FromStr for IdeDiskCli {
969    type Err = anyhow::Error;
970
971    fn from_str(s: &str) -> anyhow::Result<Self> {
972        let mut opts = s.split(',');
973        let kind = opts.next().unwrap().parse()?;
974
975        let mut read_only = false;
976        let mut channel = None;
977        let mut device = None;
978        let mut is_dvd = false;
979        for opt in opts {
980            let mut s = opt.split('=');
981            let opt = s.next().unwrap();
982            match opt {
983                "ro" => read_only = true,
984                "p" => channel = Some(0),
985                "s" => channel = Some(1),
986                "0" => device = Some(0),
987                "1" => device = Some(1),
988                "dvd" => {
989                    is_dvd = true;
990                    read_only = true;
991                }
992                _ => anyhow::bail!("unknown option: '{opt}'"),
993            }
994        }
995
996        Ok(IdeDiskCli {
997            kind,
998            read_only,
999            channel,
1000            device,
1001            is_dvd,
1002        })
1003    }
1004}
1005
1006// <kind>[,ro]
1007#[derive(Clone, Debug, PartialEq)]
1008pub struct FloppyDiskCli {
1009    pub kind: DiskCliKind,
1010    pub read_only: bool,
1011}
1012
1013impl FromStr for FloppyDiskCli {
1014    type Err = anyhow::Error;
1015
1016    fn from_str(s: &str) -> anyhow::Result<Self> {
1017        if s.is_empty() {
1018            anyhow::bail!("empty disk spec");
1019        }
1020        let mut opts = s.split(',');
1021        let kind = opts.next().unwrap().parse()?;
1022
1023        let mut read_only = false;
1024        for opt in opts {
1025            let mut s = opt.split('=');
1026            let opt = s.next().unwrap();
1027            match opt {
1028                "ro" => read_only = true,
1029                _ => anyhow::bail!("unknown option: '{opt}'"),
1030            }
1031        }
1032
1033        Ok(FloppyDiskCli { kind, read_only })
1034    }
1035}
1036
1037#[derive(Clone)]
1038pub struct DebugconSerialConfigCli {
1039    pub port: u16,
1040    pub serial: SerialConfigCli,
1041}
1042
1043impl FromStr for DebugconSerialConfigCli {
1044    type Err = String;
1045
1046    fn from_str(s: &str) -> Result<Self, Self::Err> {
1047        let Some((port, serial)) = s.split_once(',') else {
1048            return Err("invalid format (missing comma between port and serial)".into());
1049        };
1050
1051        let port: u16 = parse_number(port)
1052            .map_err(|_| "could not parse port".to_owned())?
1053            .try_into()
1054            .map_err(|_| "port must be 16-bit")?;
1055        let serial: SerialConfigCli = serial.parse()?;
1056
1057        Ok(Self { port, serial })
1058    }
1059}
1060
1061/// (console | stderr | listen=\<path\> | listen=tcp:\<ip\>:\<port\> | file=\<path\> | none)
1062#[derive(Clone, Debug, PartialEq)]
1063pub enum SerialConfigCli {
1064    None,
1065    Console,
1066    NewConsole(Option<PathBuf>, Option<String>),
1067    Stderr,
1068    Pipe(PathBuf),
1069    Tcp(SocketAddr),
1070    File(PathBuf),
1071}
1072
1073impl FromStr for SerialConfigCli {
1074    type Err = String;
1075
1076    fn from_str(s: &str) -> Result<Self, Self::Err> {
1077        let keyvalues = SerialConfigCli::parse_keyvalues(s)?;
1078
1079        let first_key = match keyvalues.first() {
1080            Some(first_pair) => first_pair.0.as_str(),
1081            None => Err("invalid serial configuration: no values supplied")?,
1082        };
1083        let first_value = keyvalues.first().unwrap().1.as_ref();
1084
1085        let ret = match first_key {
1086            "none" => SerialConfigCli::None,
1087            "console" => SerialConfigCli::Console,
1088            "stderr" => SerialConfigCli::Stderr,
1089            "file" => match first_value {
1090                Some(path) => SerialConfigCli::File(path.into()),
1091                None => Err("invalid serial configuration: file requires a value")?,
1092            },
1093            "term" => match first_value {
1094                Some(path) => {
1095                    // If user supplies a name key, use it to title the window
1096                    let window_name = keyvalues.iter().find(|(key, _)| key == "name");
1097                    let window_name = match window_name {
1098                        Some((_, Some(name))) => Some(name.clone()),
1099                        _ => None,
1100                    };
1101
1102                    SerialConfigCli::NewConsole(Some(path.into()), window_name)
1103                }
1104                None => SerialConfigCli::NewConsole(None, None),
1105            },
1106            "listen" => match first_value {
1107                Some(path) => {
1108                    if let Some(tcp) = path.strip_prefix("tcp:") {
1109                        let addr = tcp
1110                            .parse()
1111                            .map_err(|err| format!("invalid tcp address: {err}"))?;
1112                        SerialConfigCli::Tcp(addr)
1113                    } else {
1114                        SerialConfigCli::Pipe(path.into())
1115                    }
1116                }
1117                None => Err(
1118                    "invalid serial configuration: listen requires a value of tcp:addr or pipe",
1119                )?,
1120            },
1121            _ => {
1122                return Err(format!(
1123                    "invalid serial configuration: '{}' is not a known option",
1124                    first_key
1125                ));
1126            }
1127        };
1128
1129        Ok(ret)
1130    }
1131}
1132
1133impl SerialConfigCli {
1134    /// Parse a comma separated list of key=value options into a vector of
1135    /// key/value pairs.
1136    fn parse_keyvalues(s: &str) -> Result<Vec<(String, Option<String>)>, String> {
1137        let mut ret = Vec::new();
1138
1139        // For each comma separated item in the supplied list
1140        for item in s.split(',') {
1141            // Split on the = for key and value
1142            // If no = is found, treat key as key and value as None
1143            let mut eqsplit = item.split('=');
1144            let key = eqsplit.next();
1145            let value = eqsplit.next();
1146
1147            if let Some(key) = key {
1148                ret.push((key.to_owned(), value.map(|x| x.to_owned())));
1149            } else {
1150                // An empty key is invalid
1151                return Err("invalid key=value pair in serial config".into());
1152            }
1153        }
1154        Ok(ret)
1155    }
1156}
1157
1158#[derive(Clone, Debug, PartialEq)]
1159pub enum EndpointConfigCli {
1160    None,
1161    Consomme { cidr: Option<String> },
1162    Dio { id: Option<String> },
1163    Tap { name: String },
1164}
1165
1166impl FromStr for EndpointConfigCli {
1167    type Err = String;
1168
1169    fn from_str(s: &str) -> Result<Self, Self::Err> {
1170        let ret = match s.split(':').collect::<Vec<_>>().as_slice() {
1171            ["none"] => EndpointConfigCli::None,
1172            ["consomme", s @ ..] => EndpointConfigCli::Consomme {
1173                cidr: s.first().map(|&s| s.to_owned()),
1174            },
1175            ["dio", s @ ..] => EndpointConfigCli::Dio {
1176                id: s.first().map(|s| (*s).to_owned()),
1177            },
1178            ["tap", name] => EndpointConfigCli::Tap {
1179                name: (*name).to_owned(),
1180            },
1181            _ => return Err("invalid network backend".into()),
1182        };
1183
1184        Ok(ret)
1185    }
1186}
1187
1188#[derive(Clone, Debug, PartialEq)]
1189pub struct NicConfigCli {
1190    pub vtl: DeviceVtl,
1191    pub endpoint: EndpointConfigCli,
1192    pub max_queues: Option<u16>,
1193    pub underhill: bool,
1194}
1195
1196impl FromStr for NicConfigCli {
1197    type Err = String;
1198
1199    fn from_str(mut s: &str) -> Result<Self, Self::Err> {
1200        let mut vtl = DeviceVtl::Vtl0;
1201        let mut max_queues = None;
1202        let mut underhill = false;
1203        while let Some((opt, rest)) = s.split_once(':') {
1204            if let Some((opt, val)) = opt.split_once('=') {
1205                match opt {
1206                    "queues" => {
1207                        max_queues = Some(val.parse().map_err(|_| "failed to parse queue count")?);
1208                    }
1209                    _ => break,
1210                }
1211            } else {
1212                match opt {
1213                    "vtl2" => {
1214                        vtl = DeviceVtl::Vtl2;
1215                    }
1216                    "uh" => underhill = true,
1217                    _ => break,
1218                }
1219            }
1220            s = rest;
1221        }
1222
1223        if underhill && vtl != DeviceVtl::Vtl0 {
1224            return Err("`uh` is incompatible with `vtl2`".into());
1225        }
1226
1227        let endpoint = s.parse()?;
1228        Ok(NicConfigCli {
1229            vtl,
1230            endpoint,
1231            max_queues,
1232            underhill,
1233        })
1234    }
1235}
1236
1237#[derive(Debug, Error)]
1238#[error("unknown hypervisor: {0}")]
1239pub struct UnknownHypervisor(String);
1240
1241fn parse_hypervisor(s: &str) -> Result<Hypervisor, UnknownHypervisor> {
1242    match s {
1243        "kvm" => Ok(Hypervisor::Kvm),
1244        "mshv" => Ok(Hypervisor::MsHv),
1245        "whp" => Ok(Hypervisor::Whp),
1246        _ => Err(UnknownHypervisor(s.to_owned())),
1247    }
1248}
1249
1250#[derive(Debug, Error)]
1251#[error("unknown VTL2 relocation type: {0}")]
1252pub struct UnknownVtl2RelocationType(String);
1253
1254fn parse_vtl2_relocation(s: &str) -> Result<Vtl2BaseAddressType, UnknownVtl2RelocationType> {
1255    match s {
1256        "disable" => Ok(Vtl2BaseAddressType::File),
1257        s if s.starts_with("auto=") => {
1258            let s = s.strip_prefix("auto=").unwrap_or_default();
1259            let size = if s == "filesize" {
1260                None
1261            } else {
1262                let size = parse_memory(s).map_err(|e| {
1263                    UnknownVtl2RelocationType(format!(
1264                        "unable to parse memory size from {} for 'auto=' type, {e}",
1265                        e
1266                    ))
1267                })?;
1268                Some(size)
1269            };
1270            Ok(Vtl2BaseAddressType::MemoryLayout { size })
1271        }
1272        s if s.starts_with("absolute=") => {
1273            let s = s.strip_prefix("absolute=");
1274            let addr = parse_number(s.unwrap_or_default()).map_err(|e| {
1275                UnknownVtl2RelocationType(format!(
1276                    "unable to parse number from {} for 'absolute=' type",
1277                    e
1278                ))
1279            })?;
1280            Ok(Vtl2BaseAddressType::Absolute(addr))
1281        }
1282        s if s.starts_with("vtl2=") => {
1283            let s = s.strip_prefix("vtl2=").unwrap_or_default();
1284            let size = if s == "filesize" {
1285                None
1286            } else {
1287                let size = parse_memory(s).map_err(|e| {
1288                    UnknownVtl2RelocationType(format!(
1289                        "unable to parse memory size from {} for 'vtl2=' type, {e}",
1290                        e
1291                    ))
1292                })?;
1293                Some(size)
1294            };
1295            Ok(Vtl2BaseAddressType::Vtl2Allocate { size })
1296        }
1297        _ => Err(UnknownVtl2RelocationType(s.to_owned())),
1298    }
1299}
1300
1301#[derive(Debug, Copy, Clone, PartialEq)]
1302pub enum SmtConfigCli {
1303    Auto,
1304    Force,
1305    Off,
1306}
1307
1308#[derive(Debug, Error)]
1309#[error("expected auto, force, or off")]
1310pub struct BadSmtConfig;
1311
1312impl FromStr for SmtConfigCli {
1313    type Err = BadSmtConfig;
1314
1315    fn from_str(s: &str) -> Result<Self, Self::Err> {
1316        let r = match s {
1317            "auto" => Self::Auto,
1318            "force" => Self::Force,
1319            "off" => Self::Off,
1320            _ => return Err(BadSmtConfig),
1321        };
1322        Ok(r)
1323    }
1324}
1325
1326#[cfg_attr(not(guest_arch = "x86_64"), expect(dead_code))]
1327fn parse_x2apic(s: &str) -> Result<X2ApicConfig, &'static str> {
1328    let r = match s {
1329        "auto" => X2ApicConfig::Auto,
1330        "supported" => X2ApicConfig::Supported,
1331        "off" => X2ApicConfig::Unsupported,
1332        "on" => X2ApicConfig::Enabled,
1333        _ => return Err("expected auto, supported, off, or on"),
1334    };
1335    Ok(r)
1336}
1337
1338#[derive(Debug, Copy, Clone, ValueEnum)]
1339pub enum Vtl0LateMapPolicyCli {
1340    Off,
1341    Log,
1342    Halt,
1343    Exception,
1344}
1345
1346#[derive(Debug, Copy, Clone, ValueEnum)]
1347pub enum IsolationCli {
1348    Vbs,
1349}
1350
1351#[derive(Debug, Copy, Clone, PartialEq)]
1352pub struct PcatBootOrderCli(pub [PcatBootDevice; 4]);
1353
1354impl FromStr for PcatBootOrderCli {
1355    type Err = &'static str;
1356
1357    fn from_str(s: &str) -> Result<Self, Self::Err> {
1358        let mut default_order = DEFAULT_PCAT_BOOT_ORDER.map(Some);
1359        let mut order = Vec::new();
1360
1361        for item in s.split(',') {
1362            let device = match item {
1363                "optical" => PcatBootDevice::Optical,
1364                "hdd" => PcatBootDevice::HardDrive,
1365                "net" => PcatBootDevice::Network,
1366                "floppy" => PcatBootDevice::Floppy,
1367                _ => return Err("unknown boot device type"),
1368            };
1369
1370            let default_pos = default_order
1371                .iter()
1372                .position(|x| x == &Some(device))
1373                .ok_or("cannot pass duplicate boot devices")?;
1374
1375            order.push(default_order[default_pos].take().unwrap());
1376        }
1377
1378        order.extend(default_order.into_iter().flatten());
1379        assert_eq!(order.len(), 4);
1380
1381        Ok(Self(order.try_into().unwrap()))
1382    }
1383}
1384
1385#[derive(Copy, Clone, Debug, ValueEnum)]
1386pub enum UefiConsoleModeCli {
1387    Default,
1388    Com1,
1389    Com2,
1390    None,
1391}
1392
1393#[derive(Clone, Debug, PartialEq)]
1394pub struct PcieRootComplexCli {
1395    pub name: String,
1396    pub segment: u16,
1397    pub start_bus: u8,
1398    pub end_bus: u8,
1399    pub low_mmio: u32,
1400    pub high_mmio: u64,
1401}
1402
1403impl FromStr for PcieRootComplexCli {
1404    type Err = anyhow::Error;
1405
1406    fn from_str(s: &str) -> Result<Self, Self::Err> {
1407        const DEFAULT_PCIE_CRS_LOW_SIZE: u32 = 4 * 1024 * 1024; // 4M
1408        const DEFAULT_PCIE_CRS_HIGH_SIZE: u64 = 1024 * 1024 * 1024; // 1G
1409
1410        let mut opts = s.split(',');
1411        let name = opts.next().context("expected root complex name")?;
1412        if name.is_empty() {
1413            anyhow::bail!("must provide a root complex name");
1414        }
1415
1416        let mut segment = 0;
1417        let mut start_bus = 0;
1418        let mut end_bus = 255;
1419        let mut low_mmio = DEFAULT_PCIE_CRS_LOW_SIZE;
1420        let mut high_mmio = DEFAULT_PCIE_CRS_HIGH_SIZE;
1421        for opt in opts {
1422            let mut s = opt.split('=');
1423            let opt = s.next().context("expected option")?;
1424            match opt {
1425                "segment" => {
1426                    let seg_str = s.next().context("expected segment number")?;
1427                    segment = u16::from_str(seg_str).context("failed to parse segment number")?;
1428                }
1429                "start_bus" => {
1430                    let bus_str = s.next().context("expected start bus number")?;
1431                    start_bus =
1432                        u8::from_str(bus_str).context("failed to parse start bus number")?;
1433                }
1434                "end_bus" => {
1435                    let bus_str = s.next().context("expected end bus number")?;
1436                    end_bus = u8::from_str(bus_str).context("failed to parse end bus number")?;
1437                }
1438                "low_mmio" => {
1439                    let low_mmio_str = s.next().context("expected low MMIO size")?;
1440                    low_mmio = parse_memory(low_mmio_str)
1441                        .context("failed to parse low MMIO size")?
1442                        .try_into()?;
1443                }
1444                "high_mmio" => {
1445                    let high_mmio_str = s.next().context("expected high MMIO size")?;
1446                    high_mmio =
1447                        parse_memory(high_mmio_str).context("failed to parse high MMIO size")?;
1448                }
1449                opt => anyhow::bail!("unknown option: '{opt}'"),
1450            }
1451        }
1452
1453        if start_bus >= end_bus {
1454            anyhow::bail!("start_bus must be less than or equal to end_bus");
1455        }
1456
1457        Ok(PcieRootComplexCli {
1458            name: name.to_string(),
1459            segment,
1460            start_bus,
1461            end_bus,
1462            low_mmio,
1463            high_mmio,
1464        })
1465    }
1466}
1467
1468#[derive(Clone, Debug, PartialEq)]
1469pub struct PcieRootPortCli {
1470    pub root_complex_name: String,
1471    pub name: String,
1472}
1473
1474impl FromStr for PcieRootPortCli {
1475    type Err = anyhow::Error;
1476
1477    fn from_str(s: &str) -> Result<Self, Self::Err> {
1478        let mut opts = s.split(',');
1479        let names = opts.next().context("expected root port identifiers")?;
1480        if names.is_empty() {
1481            anyhow::bail!("must provide root port identifiers");
1482        }
1483
1484        let mut s = names.split(':');
1485        let rc_name = s.next().context("expected name of parent root complex")?;
1486        let rp_name = s.next().context("expected root port name")?;
1487
1488        if let Some(extra) = s.next() {
1489            anyhow::bail!("unexpected token: '{extra}'")
1490        }
1491
1492        if let Some(extra) = opts.next() {
1493            anyhow::bail!("unexpected token: '{extra}'")
1494        }
1495
1496        Ok(PcieRootPortCli {
1497            root_complex_name: rc_name.to_string(),
1498            name: rp_name.to_string(),
1499        })
1500    }
1501}
1502
1503/// Read a environment variable that may / may-not have a target-specific
1504/// prefix. e.g: `default_value_from_arch_env("FOO")` would first try and read
1505/// from `FOO`, and if that's not found, it will try `X86_64_FOO`.
1506///
1507/// Must return an `OsString`, in order to be compatible with `clap`'s
1508/// default_value code. As such - to encode the absence of the env-var, an empty
1509/// OsString is returned.
1510fn default_value_from_arch_env(name: &str) -> OsString {
1511    let prefix = if cfg!(guest_arch = "x86_64") {
1512        "X86_64"
1513    } else if cfg!(guest_arch = "aarch64") {
1514        "AARCH64"
1515    } else {
1516        return Default::default();
1517    };
1518    let prefixed = format!("{}_{}", prefix, name);
1519    std::env::var_os(name)
1520        .or_else(|| std::env::var_os(prefixed))
1521        .unwrap_or_default()
1522}
1523
1524/// Workaround to use `Option<PathBuf>` alongside [`default_value_from_arch_env`]
1525#[derive(Clone)]
1526pub struct OptionalPathBuf(pub Option<PathBuf>);
1527
1528impl From<&std::ffi::OsStr> for OptionalPathBuf {
1529    fn from(s: &std::ffi::OsStr) -> Self {
1530        OptionalPathBuf(if s.is_empty() { None } else { Some(s.into()) })
1531    }
1532}
1533
1534#[cfg(test)]
1535// UNSAFETY: Needed to set and remove environment variables in tests
1536#[expect(unsafe_code)]
1537mod tests {
1538    use super::*;
1539
1540    fn with_env_var<F, R>(name: &str, value: &str, f: F) -> R
1541    where
1542        F: FnOnce() -> R,
1543    {
1544        // SAFETY:
1545        // Safe in a testing context because it won't be changed concurrently
1546        unsafe {
1547            std::env::set_var(name, value);
1548        }
1549        let result = f();
1550        // SAFETY:
1551        // Safe in a testing context because it won't be changed concurrently
1552        unsafe {
1553            std::env::remove_var(name);
1554        }
1555        result
1556    }
1557
1558    #[test]
1559    fn test_parse_file_disk_with_create() {
1560        let s = "file:test.vhd;create=1G";
1561        let disk = DiskCliKind::from_str(s).unwrap();
1562
1563        match disk {
1564            DiskCliKind::File {
1565                path,
1566                create_with_len,
1567            } => {
1568                assert_eq!(path, PathBuf::from("test.vhd"));
1569                assert_eq!(create_with_len, Some(1024 * 1024 * 1024)); // 1G
1570            }
1571            _ => panic!("Expected File variant"),
1572        }
1573    }
1574
1575    #[test]
1576    fn test_parse_direct_file_with_create() {
1577        let s = "test.vhd;create=1G";
1578        let disk = DiskCliKind::from_str(s).unwrap();
1579
1580        match disk {
1581            DiskCliKind::File {
1582                path,
1583                create_with_len,
1584            } => {
1585                assert_eq!(path, PathBuf::from("test.vhd"));
1586                assert_eq!(create_with_len, Some(1024 * 1024 * 1024)); // 1G
1587            }
1588            _ => panic!("Expected File variant"),
1589        }
1590    }
1591
1592    #[test]
1593    fn test_parse_memory_disk() {
1594        let s = "mem:1G";
1595        let disk = DiskCliKind::from_str(s).unwrap();
1596        match disk {
1597            DiskCliKind::Memory(size) => {
1598                assert_eq!(size, 1024 * 1024 * 1024); // 1G
1599            }
1600            _ => panic!("Expected Memory variant"),
1601        }
1602    }
1603
1604    #[test]
1605    fn test_parse_memory_diff_disk() {
1606        let s = "memdiff:file:base.img";
1607        let disk = DiskCliKind::from_str(s).unwrap();
1608        match disk {
1609            DiskCliKind::MemoryDiff(inner) => match *inner {
1610                DiskCliKind::File {
1611                    path,
1612                    create_with_len,
1613                } => {
1614                    assert_eq!(path, PathBuf::from("base.img"));
1615                    assert_eq!(create_with_len, None);
1616                }
1617                _ => panic!("Expected File variant inside MemoryDiff"),
1618            },
1619            _ => panic!("Expected MemoryDiff variant"),
1620        }
1621    }
1622
1623    #[test]
1624    fn test_parse_sqlite_disk() {
1625        let s = "sql:db.sqlite;create=2G";
1626        let disk = DiskCliKind::from_str(s).unwrap();
1627        match disk {
1628            DiskCliKind::Sqlite {
1629                path,
1630                create_with_len,
1631            } => {
1632                assert_eq!(path, PathBuf::from("db.sqlite"));
1633                assert_eq!(create_with_len, Some(2 * 1024 * 1024 * 1024));
1634            }
1635            _ => panic!("Expected Sqlite variant"),
1636        }
1637
1638        // Test without create option
1639        let s = "sql:db.sqlite";
1640        let disk = DiskCliKind::from_str(s).unwrap();
1641        match disk {
1642            DiskCliKind::Sqlite {
1643                path,
1644                create_with_len,
1645            } => {
1646                assert_eq!(path, PathBuf::from("db.sqlite"));
1647                assert_eq!(create_with_len, None);
1648            }
1649            _ => panic!("Expected Sqlite variant"),
1650        }
1651    }
1652
1653    #[test]
1654    fn test_parse_sqlite_diff_disk() {
1655        // Test with create option
1656        let s = "sqldiff:diff.sqlite;create:file:base.img";
1657        let disk = DiskCliKind::from_str(s).unwrap();
1658        match disk {
1659            DiskCliKind::SqliteDiff { path, create, disk } => {
1660                assert_eq!(path, PathBuf::from("diff.sqlite"));
1661                assert!(create);
1662                match *disk {
1663                    DiskCliKind::File {
1664                        path,
1665                        create_with_len,
1666                    } => {
1667                        assert_eq!(path, PathBuf::from("base.img"));
1668                        assert_eq!(create_with_len, None);
1669                    }
1670                    _ => panic!("Expected File variant inside SqliteDiff"),
1671                }
1672            }
1673            _ => panic!("Expected SqliteDiff variant"),
1674        }
1675
1676        // Test without create option
1677        let s = "sqldiff:diff.sqlite:file:base.img";
1678        let disk = DiskCliKind::from_str(s).unwrap();
1679        match disk {
1680            DiskCliKind::SqliteDiff { path, create, disk } => {
1681                assert_eq!(path, PathBuf::from("diff.sqlite"));
1682                assert!(!create);
1683                match *disk {
1684                    DiskCliKind::File {
1685                        path,
1686                        create_with_len,
1687                    } => {
1688                        assert_eq!(path, PathBuf::from("base.img"));
1689                        assert_eq!(create_with_len, None);
1690                    }
1691                    _ => panic!("Expected File variant inside SqliteDiff"),
1692                }
1693            }
1694            _ => panic!("Expected SqliteDiff variant"),
1695        }
1696    }
1697
1698    #[test]
1699    fn test_parse_autocache_sqlite_disk() {
1700        // Test with environment variable set
1701        let disk = with_env_var("OPENVMM_AUTO_CACHE_PATH", "/tmp/cache", || {
1702            DiskCliKind::from_str("autocache::file:disk.vhd").unwrap()
1703        });
1704        assert!(matches!(
1705            disk,
1706            DiskCliKind::AutoCacheSqlite {
1707                cache_path,
1708                key,
1709                disk: _disk,
1710            } if cache_path == "/tmp/cache" && key.is_none()
1711        ));
1712
1713        // Test without environment variable
1714        assert!(DiskCliKind::from_str("autocache::file:disk.vhd").is_err());
1715    }
1716
1717    #[test]
1718    fn test_parse_disk_errors() {
1719        assert!(DiskCliKind::from_str("invalid:").is_err());
1720        assert!(DiskCliKind::from_str("memory:extra").is_err());
1721
1722        // Test sqlite: without environment variable
1723        assert!(DiskCliKind::from_str("sqlite:").is_err());
1724    }
1725
1726    #[test]
1727    fn test_parse_errors() {
1728        // Invalid memory size
1729        assert!(DiskCliKind::from_str("mem:invalid").is_err());
1730
1731        // Invalid syntax for SQLiteDiff
1732        assert!(DiskCliKind::from_str("sqldiff:path").is_err());
1733
1734        // Missing OPENVMM_AUTO_CACHE_PATH for AutoCacheSqlite
1735        // SAFETY:
1736        // Safe in a testing context because it won't be changed concurrently
1737        unsafe {
1738            std::env::remove_var("OPENVMM_AUTO_CACHE_PATH");
1739        }
1740        assert!(DiskCliKind::from_str("autocache:key:file:disk.vhd").is_err());
1741
1742        // Invalid blob kind
1743        assert!(DiskCliKind::from_str("blob:invalid:url").is_err());
1744
1745        // Invalid cipher
1746        assert!(DiskCliKind::from_str("crypt:invalid:key.bin:file:disk.vhd").is_err());
1747
1748        // Invalid format for crypt (missing parts)
1749        assert!(DiskCliKind::from_str("crypt:xts-aes-256:key.bin").is_err());
1750
1751        // Invalid disk kind
1752        assert!(DiskCliKind::from_str("invalid:path").is_err());
1753
1754        // Missing create size
1755        assert!(DiskCliKind::from_str("file:disk.vhd;create=").is_err());
1756    }
1757
1758    #[test]
1759    fn test_fs_args_from_str() {
1760        let args = FsArgs::from_str("tag1,/path/to/fs").unwrap();
1761        assert_eq!(args.tag, "tag1");
1762        assert_eq!(args.path, "/path/to/fs");
1763
1764        // Test error cases
1765        assert!(FsArgs::from_str("tag1").is_err());
1766        assert!(FsArgs::from_str("tag1,/path,extra").is_err());
1767    }
1768
1769    #[test]
1770    fn test_fs_args_with_options_from_str() {
1771        let args = FsArgsWithOptions::from_str("tag1,/path/to/fs,opt1,opt2").unwrap();
1772        assert_eq!(args.tag, "tag1");
1773        assert_eq!(args.path, "/path/to/fs");
1774        assert_eq!(args.options, "opt1;opt2");
1775
1776        // Test without options
1777        let args = FsArgsWithOptions::from_str("tag1,/path/to/fs").unwrap();
1778        assert_eq!(args.tag, "tag1");
1779        assert_eq!(args.path, "/path/to/fs");
1780        assert_eq!(args.options, "");
1781
1782        // Test error case
1783        assert!(FsArgsWithOptions::from_str("tag1").is_err());
1784    }
1785
1786    #[test]
1787    fn test_serial_config_from_str() {
1788        assert_eq!(
1789            SerialConfigCli::from_str("none").unwrap(),
1790            SerialConfigCli::None
1791        );
1792        assert_eq!(
1793            SerialConfigCli::from_str("console").unwrap(),
1794            SerialConfigCli::Console
1795        );
1796        assert_eq!(
1797            SerialConfigCli::from_str("stderr").unwrap(),
1798            SerialConfigCli::Stderr
1799        );
1800
1801        // Test file config
1802        let file_config = SerialConfigCli::from_str("file=/path/to/file").unwrap();
1803        if let SerialConfigCli::File(path) = file_config {
1804            assert_eq!(path.to_str().unwrap(), "/path/to/file");
1805        } else {
1806            panic!("Expected File variant");
1807        }
1808
1809        // Test term config with name
1810        match SerialConfigCli::from_str("term=/dev/pts/0,name=MyTerm").unwrap() {
1811            SerialConfigCli::NewConsole(Some(path), Some(name)) => {
1812                assert_eq!(path.to_str().unwrap(), "/dev/pts/0");
1813                assert_eq!(name, "MyTerm");
1814            }
1815            _ => panic!("Expected NewConsole variant with name"),
1816        }
1817
1818        // Test term config without name
1819        match SerialConfigCli::from_str("term=/dev/pts/0").unwrap() {
1820            SerialConfigCli::NewConsole(Some(path), None) => {
1821                assert_eq!(path.to_str().unwrap(), "/dev/pts/0");
1822            }
1823            _ => panic!("Expected NewConsole variant without name"),
1824        }
1825
1826        // Test TCP config
1827        match SerialConfigCli::from_str("listen=tcp:127.0.0.1:1234").unwrap() {
1828            SerialConfigCli::Tcp(addr) => {
1829                assert_eq!(addr.to_string(), "127.0.0.1:1234");
1830            }
1831            _ => panic!("Expected Tcp variant"),
1832        }
1833
1834        // Test pipe config
1835        match SerialConfigCli::from_str("listen=/path/to/pipe").unwrap() {
1836            SerialConfigCli::Pipe(path) => {
1837                assert_eq!(path.to_str().unwrap(), "/path/to/pipe");
1838            }
1839            _ => panic!("Expected Pipe variant"),
1840        }
1841
1842        // Test error cases
1843        assert!(SerialConfigCli::from_str("").is_err());
1844        assert!(SerialConfigCli::from_str("unknown").is_err());
1845        assert!(SerialConfigCli::from_str("file").is_err());
1846        assert!(SerialConfigCli::from_str("listen").is_err());
1847    }
1848
1849    #[test]
1850    fn test_endpoint_config_from_str() {
1851        // Test none
1852        assert!(matches!(
1853            EndpointConfigCli::from_str("none").unwrap(),
1854            EndpointConfigCli::None
1855        ));
1856
1857        // Test consomme without cidr
1858        match EndpointConfigCli::from_str("consomme").unwrap() {
1859            EndpointConfigCli::Consomme { cidr: None } => (),
1860            _ => panic!("Expected Consomme variant without cidr"),
1861        }
1862
1863        // Test consomme with cidr
1864        match EndpointConfigCli::from_str("consomme:192.168.0.0/24").unwrap() {
1865            EndpointConfigCli::Consomme { cidr: Some(cidr) } => {
1866                assert_eq!(cidr, "192.168.0.0/24");
1867            }
1868            _ => panic!("Expected Consomme variant with cidr"),
1869        }
1870
1871        // Test dio without id
1872        match EndpointConfigCli::from_str("dio").unwrap() {
1873            EndpointConfigCli::Dio { id: None } => (),
1874            _ => panic!("Expected Dio variant without id"),
1875        }
1876
1877        // Test dio with id
1878        match EndpointConfigCli::from_str("dio:test_id").unwrap() {
1879            EndpointConfigCli::Dio { id: Some(id) } => {
1880                assert_eq!(id, "test_id");
1881            }
1882            _ => panic!("Expected Dio variant with id"),
1883        }
1884
1885        // Test tap
1886        match EndpointConfigCli::from_str("tap:tap0").unwrap() {
1887            EndpointConfigCli::Tap { name } => {
1888                assert_eq!(name, "tap0");
1889            }
1890            _ => panic!("Expected Tap variant"),
1891        }
1892
1893        // Test error case
1894        assert!(EndpointConfigCli::from_str("invalid").is_err());
1895    }
1896
1897    #[test]
1898    fn test_nic_config_from_str() {
1899        use hvlite_defs::config::DeviceVtl;
1900
1901        // Test basic endpoint
1902        let config = NicConfigCli::from_str("none").unwrap();
1903        assert_eq!(config.vtl, DeviceVtl::Vtl0);
1904        assert!(config.max_queues.is_none());
1905        assert!(!config.underhill);
1906        assert!(matches!(config.endpoint, EndpointConfigCli::None));
1907
1908        // Test with vtl2
1909        let config = NicConfigCli::from_str("vtl2:none").unwrap();
1910        assert_eq!(config.vtl, DeviceVtl::Vtl2);
1911        assert!(matches!(config.endpoint, EndpointConfigCli::None));
1912
1913        // Test with queues
1914        let config = NicConfigCli::from_str("queues=4:none").unwrap();
1915        assert_eq!(config.max_queues, Some(4));
1916        assert!(matches!(config.endpoint, EndpointConfigCli::None));
1917
1918        // Test with underhill
1919        let config = NicConfigCli::from_str("uh:none").unwrap();
1920        assert!(config.underhill);
1921        assert!(matches!(config.endpoint, EndpointConfigCli::None));
1922
1923        // Test error cases
1924        assert!(NicConfigCli::from_str("queues=invalid:none").is_err());
1925        assert!(NicConfigCli::from_str("uh:vtl2:none").is_err()); // uh incompatible with vtl2
1926    }
1927
1928    #[test]
1929    fn test_smt_config_from_str() {
1930        assert_eq!(SmtConfigCli::from_str("auto").unwrap(), SmtConfigCli::Auto);
1931        assert_eq!(
1932            SmtConfigCli::from_str("force").unwrap(),
1933            SmtConfigCli::Force
1934        );
1935        assert_eq!(SmtConfigCli::from_str("off").unwrap(), SmtConfigCli::Off);
1936
1937        // Test error cases
1938        assert!(SmtConfigCli::from_str("invalid").is_err());
1939        assert!(SmtConfigCli::from_str("").is_err());
1940    }
1941
1942    #[test]
1943    fn test_pcat_boot_order_from_str() {
1944        // Test single device
1945        let order = PcatBootOrderCli::from_str("optical").unwrap();
1946        assert_eq!(order.0[0], PcatBootDevice::Optical);
1947
1948        // Test multiple devices
1949        let order = PcatBootOrderCli::from_str("hdd,net").unwrap();
1950        assert_eq!(order.0[0], PcatBootDevice::HardDrive);
1951        assert_eq!(order.0[1], PcatBootDevice::Network);
1952
1953        // Test error cases
1954        assert!(PcatBootOrderCli::from_str("invalid").is_err());
1955        assert!(PcatBootOrderCli::from_str("optical,optical").is_err()); // duplicate device
1956    }
1957
1958    #[test]
1959    fn test_floppy_disk_from_str() {
1960        // Test basic disk
1961        let disk = FloppyDiskCli::from_str("file:/path/to/floppy.img").unwrap();
1962        assert!(!disk.read_only);
1963        match disk.kind {
1964            DiskCliKind::File {
1965                path,
1966                create_with_len,
1967            } => {
1968                assert_eq!(path.to_str().unwrap(), "/path/to/floppy.img");
1969                assert_eq!(create_with_len, None);
1970            }
1971            _ => panic!("Expected File variant"),
1972        }
1973
1974        // Test with read-only flag
1975        let disk = FloppyDiskCli::from_str("file:/path/to/floppy.img,ro").unwrap();
1976        assert!(disk.read_only);
1977
1978        // Test error cases
1979        assert!(FloppyDiskCli::from_str("").is_err());
1980        assert!(FloppyDiskCli::from_str("file:/path/to/floppy.img,invalid").is_err());
1981    }
1982
1983    #[test]
1984    fn test_pcie_root_complex_from_str() {
1985        const ONE_MB: u64 = 1024 * 1024;
1986        const ONE_GB: u64 = 1024 * ONE_MB;
1987
1988        const DEFAULT_LOW_MMIO: u32 = (4 * ONE_MB) as u32;
1989        const DEFAULT_HIGH_MMIO: u64 = ONE_GB;
1990
1991        assert_eq!(
1992            PcieRootComplexCli::from_str("rc0").unwrap(),
1993            PcieRootComplexCli {
1994                name: "rc0".to_string(),
1995                segment: 0,
1996                start_bus: 0,
1997                end_bus: 255,
1998                low_mmio: DEFAULT_LOW_MMIO,
1999                high_mmio: DEFAULT_HIGH_MMIO,
2000            }
2001        );
2002
2003        assert_eq!(
2004            PcieRootComplexCli::from_str("rc1,segment=1").unwrap(),
2005            PcieRootComplexCli {
2006                name: "rc1".to_string(),
2007                segment: 1,
2008                start_bus: 0,
2009                end_bus: 255,
2010                low_mmio: DEFAULT_LOW_MMIO,
2011                high_mmio: DEFAULT_HIGH_MMIO,
2012            }
2013        );
2014
2015        assert_eq!(
2016            PcieRootComplexCli::from_str("rc2,start_bus=32").unwrap(),
2017            PcieRootComplexCli {
2018                name: "rc2".to_string(),
2019                segment: 0,
2020                start_bus: 32,
2021                end_bus: 255,
2022                low_mmio: DEFAULT_LOW_MMIO,
2023                high_mmio: DEFAULT_HIGH_MMIO,
2024            }
2025        );
2026
2027        assert_eq!(
2028            PcieRootComplexCli::from_str("rc3,end_bus=31").unwrap(),
2029            PcieRootComplexCli {
2030                name: "rc3".to_string(),
2031                segment: 0,
2032                start_bus: 0,
2033                end_bus: 31,
2034                low_mmio: DEFAULT_LOW_MMIO,
2035                high_mmio: DEFAULT_HIGH_MMIO,
2036            }
2037        );
2038
2039        assert_eq!(
2040            PcieRootComplexCli::from_str("rc4,start_bus=32,end_bus=127,high_mmio=2G").unwrap(),
2041            PcieRootComplexCli {
2042                name: "rc4".to_string(),
2043                segment: 0,
2044                start_bus: 32,
2045                end_bus: 127,
2046                low_mmio: DEFAULT_LOW_MMIO,
2047                high_mmio: 2 * ONE_GB,
2048            }
2049        );
2050
2051        assert_eq!(
2052            PcieRootComplexCli::from_str("rc5,segment=2,start_bus=32,end_bus=127").unwrap(),
2053            PcieRootComplexCli {
2054                name: "rc5".to_string(),
2055                segment: 2,
2056                start_bus: 32,
2057                end_bus: 127,
2058                low_mmio: DEFAULT_LOW_MMIO,
2059                high_mmio: DEFAULT_HIGH_MMIO,
2060            }
2061        );
2062
2063        assert_eq!(
2064            PcieRootComplexCli::from_str("rc6,low_mmio=1M,high_mmio=64G").unwrap(),
2065            PcieRootComplexCli {
2066                name: "rc6".to_string(),
2067                segment: 0,
2068                start_bus: 0,
2069                end_bus: 255,
2070                low_mmio: ONE_MB as u32,
2071                high_mmio: 64 * ONE_GB,
2072            }
2073        );
2074
2075        // Error cases
2076        assert!(PcieRootComplexCli::from_str("").is_err());
2077        assert!(PcieRootComplexCli::from_str("poorly,").is_err());
2078        assert!(PcieRootComplexCli::from_str("configured,complex").is_err());
2079        assert!(PcieRootComplexCli::from_str("fails,start_bus=foo").is_err());
2080        assert!(PcieRootComplexCli::from_str("fails,start_bus=32,end_bus=31").is_err());
2081        assert!(PcieRootComplexCli::from_str("rc,start_bus=256").is_err());
2082        assert!(PcieRootComplexCli::from_str("rc,end_bus=256").is_err());
2083        assert!(PcieRootComplexCli::from_str("rc,low_mmio=5G").is_err());
2084        assert!(PcieRootComplexCli::from_str("rc,low_mmio=aG").is_err());
2085        assert!(PcieRootComplexCli::from_str("rc,high_mmio=bad").is_err());
2086        assert!(PcieRootComplexCli::from_str("rc,high_mmio").is_err());
2087    }
2088
2089    #[test]
2090    fn test_pcie_root_port_from_str() {
2091        assert_eq!(
2092            PcieRootPortCli::from_str("rc0:rc0rp0").unwrap(),
2093            PcieRootPortCli {
2094                root_complex_name: "rc0".to_string(),
2095                name: "rc0rp0".to_string()
2096            }
2097        );
2098
2099        assert_eq!(
2100            PcieRootPortCli::from_str("my_rc:port2").unwrap(),
2101            PcieRootPortCli {
2102                root_complex_name: "my_rc".to_string(),
2103                name: "port2".to_string()
2104            }
2105        );
2106
2107        // Error cases
2108        assert!(PcieRootPortCli::from_str("").is_err());
2109        assert!(PcieRootPortCli::from_str("rp0").is_err());
2110        assert!(PcieRootPortCli::from_str("rp0,opt").is_err());
2111        assert!(PcieRootPortCli::from_str("rc0:rp0:rp3").is_err());
2112        assert!(PcieRootPortCli::from_str("rc0:rp0,rp3").is_err());
2113    }
2114}