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