// Copyright © 2019 Intel Corporation
//
// SPDX-License-Identifier: Apache-2.0
//

use std::collections::{BTreeSet, HashMap, HashSet};
#[cfg(feature = "ivshmem")]
use std::fs;
use std::path::PathBuf;
use std::result;
use std::str::FromStr;
use std::sync::LazyLock;

use arch::CpuProfile;
use block::ImageType;
use clap::ArgMatches;
use log::{debug, warn};
use option_parser::{
    ByteSized, IntegerList, OptionParser, OptionParserError, StringList, Toggle, Tuple,
};
use pci::NUM_DEVICE_IDS;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use virtio_bindings::virtio_blk::VIRTIO_BLK_ID_BYTES;
use virtio_bindings::virtio_ids::*;
use virtio_devices::block::MINIMUM_BLOCK_QUEUE_SIZE;
use virtio_devices::vhost_user::VIRTIO_FS_TAG_LEN;
use virtio_devices::{RateLimiterConfig, TokenBucketConfig, net, vhost_user};

use crate::landlock::LandlockAccess;
use crate::vm_config::*;

const MAX_NUM_PCI_SEGMENTS: u16 = 96;
const MAX_IOMMU_ADDRESS_WIDTH_BITS: u8 = 64;

// Maximum queue size is largest power of 2 that fits into a u16
const VIRTIO_MAX_QUEUE_SIZE: u16 = 32768;

#[cfg(all(feature = "kvm", target_arch = "x86_64"))]
const MAX_SUPPORTED_CPUS: u32 = 8192;
#[cfg(not(all(feature = "kvm", target_arch = "x86_64")))]
const MAX_SUPPORTED_CPUS: u32 = 255;

/// Errors associated with VM configuration parameters.
#[derive(Debug, Error)]
pub enum Error {
    /// Filesystem tag is missing
    #[error("Error parsing --fs: tag missing")]
    ParseFsTagMissing,
    /// Filesystem tag is too long
    #[error("Error parsing --fs: max tag length is {VIRTIO_FS_TAG_LEN}")]
    ParseFsTagTooLong,
    /// Filesystem socket is missing
    #[error("Error parsing --fs: socket missing")]
    ParseFsSockMissing,
    /// Generic vhost-user device type is invalid
    #[error(
        "Error parsing --generic-vhost-user: device_type {0:?} invalid (leading zeros or unknown string)"
    )]
    ParseGenericVhostUserVirtioIdInvalid(String),
    /// Generic vhost-user device type is unsupported
    #[error(
        "Error parsing --generic-vhost-user: device with device_type {0:?} cannot be implemented via vhost-user"
    )]
    ParseGenericVhostUserVirtioIdUnsupported(String),
    /// Generic vhost-user socket is missing
    #[error("Error parsing --generic-vhost-user: socket missing")]
    ParseGenericVhostUserSockMissing,
    /// Generic vhost-user number of queues is missing
    #[error("Error parsing --generic-vhost-user: number of queues missing")]
    ParseGenericVhostUserNumResponseQueuesMissing,
    /// Generic vhost-user device type is missing
    #[error("Error parsing --generic-vhost-user: device_type missing")]
    ParseGenericVhostUserVirtioIdMissing,
    /// Generic vhost-user available features is missing
    #[error("Error parsing --generic-vhost-user: available features missing")]
    ParseGenericVhostUserAvailFeaturesMissing,
    /// Generic vhost-user queue size missing
    #[error("Error parsing --generic-vhost-user: queue size missing")]
    ParseGenericVhostUserQueueSizeMissing,
    /// Missing persistent memory file parameter.
    #[error("Error parsing --pmem: file missing")]
    ParsePmemFileMissing,
    /// Missing vsock socket path parameter.
    #[error("Error parsing --vsock: socket missing")]
    ParseVsockSockMissing,
    /// Missing vsock cid parameter.
    #[error("Error parsing --vsock: cid missing")]
    ParseVsockCidMissing,
    /// Missing restore source_url parameter.
    #[error("Error parsing --restore: source_url missing")]
    ParseRestoreSourceUrlMissing,
    /// Error parsing CPU options
    #[error("Error parsing --cpus")]
    ParseCpus(#[source] OptionParserError),
    /// Invalid CPU features
    #[error("Invalid feature in --cpus features list: {0}")]
    InvalidCpuFeatures(String),
    /// Error parsing memory options
    #[error("Error parsing --memory")]
    ParseMemory(#[source] OptionParserError),
    /// Error parsing memory zone options
    #[error("Error parsing --memory-zone")]
    ParseMemoryZone(#[source] OptionParserError),
    /// Missing 'id' from memory zone
    #[error("Error parsing --memory-zone: id missing")]
    ParseMemoryZoneIdMissing,
    /// Error parsing rate-limiter group options
    #[error("Error parsing --rate-limit-group")]
    ParseRateLimiterGroup(#[source] OptionParserError),
    /// Error parsing disk options
    #[error("Error parsing --disk")]
    ParseDisk(#[source] OptionParserError),
    /// Error parsing network options
    #[error("Error parsing --net")]
    ParseNetwork(#[source] OptionParserError),
    /// Error parsing RNG options
    #[error("Error parsing --rng")]
    ParseRng(#[source] OptionParserError),
    /// Error parsing RTC options
    #[error("Error parsing --rtc")]
    ParseRtc(#[source] OptionParserError),
    /// Error parsing balloon options
    #[error("Error parsing --balloon")]
    ParseBalloon(#[source] OptionParserError),
    /// Error parsing filesystem parameters
    #[error("Error parsing --fs")]
    ParseFileSystem(#[source] OptionParserError),
    /// Error parsing persistent memory parameters
    #[error("Error parsing --pmem")]
    ParsePersistentMemory(#[source] OptionParserError),
    /// Error parsing generic vhost-user parameters
    #[error("Error parsing --generic-vhost-user")]
    ParseGenericVhostUser(#[source] OptionParserError),
    /// Failed parsing console parameters
    #[error("Error parsing --console")]
    ParseConsole(#[source] OptionParserError),
    /// Failed parsing serial parameters
    #[error("Error parsing --serial")]
    ParseSerial(#[source] OptionParserError),
    #[cfg(target_arch = "x86_64")]
    /// Failed parsing debug-console
    #[error("Error parsing --debug-console")]
    ParseDebugConsole(#[source] OptionParserError),
    /// No mode given for console
    #[error("Error parsing --console: invalid console mode given")]
    ParseConsoleInvalidModeGiven,
    /// Failed parsing device parameters
    #[error("Error parsing --device")]
    ParseDevice(#[source] OptionParserError),
    /// Failed parsing vsock parameters
    #[error("Error parsing --vsock")]
    ParseVsock(#[source] OptionParserError),
    /// Failed parsing restore parameters
    #[error("Error parsing --restore")]
    ParseRestore(#[source] OptionParserError),
    /// Failed parsing NUMA parameters
    #[error("Error parsing --numa")]
    ParseNuma(#[source] OptionParserError),
    /// Failed validating configuration
    #[error("Error validating configuration")]
    Validation(#[source] ValidationError),
    #[cfg(feature = "sev_snp")]
    #[error("Error parsing --sev_snp")]
    /// Failed parsing SEV-SNP config
    ParseSevSnp(#[source] OptionParserError),
    #[cfg(feature = "tdx")]
    #[error("Error parsing --tdx")]
    /// Failed parsing TDX config
    ParseTdx(#[source] OptionParserError),
    #[cfg(feature = "tdx")]
    #[error("TDX firmware missing")]
    /// No TDX firmware
    FirmwarePathMissing,
    /// Failed parsing userspace device
    #[error("Error parsing --user-device")]
    ParseUserDevice(#[source] OptionParserError),
    /// Missing socket for userspace device
    #[error("Error parsing --user-device: socket missing")]
    ParseUserDeviceSocketMissing,
    /// Error parsing pci segment options
    #[error("Error parsing --pci-segment")]
    ParsePciSegment(#[source] OptionParserError),
    /// Failed parsing platform parameters
    #[error("Error parsing --platform")]
    ParsePlatform(#[source] OptionParserError),
    /// Failed parsing vDPA device
    #[error("Error parsing --vdpa")]
    ParseVdpa(#[source] OptionParserError),
    /// Missing path for vDPA device
    #[error("Error parsing --vdpa: path missing")]
    ParseVdpaPathMissing,
    /// Failed parsing TPM device
    #[error("Error parsing --tpm")]
    ParseTpm(#[source] OptionParserError),
    #[cfg(feature = "ivshmem")]
    /// Failed parsing ivsmem device
    #[error("Error parsing --ivshmem")]
    ParseIvshmem(#[source] OptionParserError),
    /// Missing path for TPM device
    #[error("Error parsing --tpm: path missing")]
    ParseTpmPathMissing,
    #[cfg(feature = "ivshmem")]
    /// Missing path for ivsmem device
    #[error("Error parsing --ivshmem: path missing")]
    ParseIvshmemPathMissing,
    /// Error parsing Landlock rules
    #[error("Error parsing --landlock-rules")]
    ParseLandlockRules(#[source] OptionParserError),
    /// Missing fields in Landlock rules
    #[error("Error parsing --landlock-rules: path/access field missing")]
    ParseLandlockMissingFields,
    #[cfg(feature = "fw_cfg")]
    /// Failed Parsing FwCfgItem config
    #[error("Error parsing --fw-cfg-config items")]
    ParseFwCfgItem(#[source] OptionParserError),
    #[error("Error parsing common PCI device config")]
    ParsePciDeviceCommonConfig(#[source] OptionParserError),
}

#[derive(Debug, PartialEq, Eq, Error)]
pub enum ValidationError {
    /// Missing file value for console
    #[error("Path missing when using file console mode")]
    ConsoleFileMissing,
    /// Missing socket path for console
    #[error("Path missing when using socket console mode")]
    ConsoleSocketPathMissing,
    /// Max is less than boot
    #[error("Max CPUs ({0}) lower than boot CPUs ({1})")]
    CpusMaxLowerThanBoot(u32 /* max vCPUs */, u32 /* boot vCPUs */),
    /// Too many CPUs.
    #[error("Too many CPUs: specified {0} but {MAX_SUPPORTED_CPUS} is the limit")]
    TooManyCpus(u32 /* specified CPUs */),
    /// Missing file value for debug-console
    #[cfg(target_arch = "x86_64")]
    #[error("Path missing when using file mode for debug console")]
    DebugconFileMissing,
    /// Both socket and path specified
    #[error("Disk path and vhost socket both provided")]
    DiskSocketAndPath,
    /// Using vhost user requires shared memory
    #[error("Using vhost-user requires using shared memory or huge pages")]
    VhostUserRequiresSharedMemory,
    /// No socket provided for vhost_use
    #[error("No socket provided when using vhost-user")]
    VhostUserMissingSocket,
    /// Trying to use IOMMU without PCI
    #[error("Using an IOMMU without PCI support is unsupported")]
    IommuUnsupported,
    /// Trying to use VFIO without PCI
    #[error("Using VFIO without PCI support is unsupported")]
    VfioUnsupported,
    /// CPU topology count doesn't match max
    #[error("Product of CPU topology parts does not match maximum vCPU")]
    CpuTopologyCount,
    /// CPU topology uses too many threads per core
    #[error("CPU topology supports at most 2 threads per core")]
    CpuTopologyThreadsPerCore,
    /// One part of the CPU topology was zero
    #[error("No part of the CPU topology can be zero")]
    CpuTopologyZeroPart,
    #[cfg(target_arch = "aarch64")]
    /// Dies per package must be 1
    #[error("Dies per package must be 1")]
    CpuTopologyDiesPerPackage,
    /// Virtio needs a min of 2 queues
    #[error("Number of queues ({0}) to virtio_net should be higher than 2")]
    VnetQueueLowerThan2(usize),
    /// The input queue number for virtio_net must match the number of input fds
    #[error("Number of queues ({0}) to virtio_net does not match the number of FDs ({1})")]
    VnetQueueFdMismatch(usize /* num of queues */, usize /* FD num */),
    /// Using reserved fd
    #[error("Reserved fd number (<= 2): {0}")]
    VnetReservedFd(i32),
    /// Hardware checksum offload is disabled.
    #[error("\"offload_tso\" and \"offload_ufo\" depend on \"offload_csum\"")]
    NoHardwareChecksumOffload,
    /// Hugepages not turned on
    #[error("Huge page size specified but huge pages not enabled")]
    HugePageSizeWithoutHugePages,
    /// Huge page size is not power of 2
    #[error("Huge page size is not power of 2: {0}")]
    InvalidHugePageSize(u64),
    /// CPU Hotplug is not permitted with TDX
    #[cfg(feature = "tdx")]
    #[error("CPU hotplug is not permitted with TDX")]
    TdxNoCpuHotplug,
    /// Missing firmware for TDX
    #[cfg(feature = "tdx")]
    #[error("No TDX firmware specified")]
    TdxFirmwareMissing,
    /// Insufficient vCPUs for queues
    #[error("Queue count ({0}) must not exceed boot vCPUs ({1})")]
    TooManyQueues(usize /* queues */, usize /* vCPUs */),
    /// Queue size is not a power of 2 within the spec-permitted range
    #[error("Queue size must be a power of 2 no greater than {VIRTIO_MAX_QUEUE_SIZE}: {0}")]
    InvalidQueueSize(u16),
    /// Block queue size too small to advertise a usable seg_max
    #[error("Block queue size must be greater than {MINIMUM_BLOCK_QUEUE_SIZE}: {0}")]
    BlockQueueSizeTooSmall(u16),
    /// Need shared memory for vfio-user
    #[error("Using user devices requires using shared memory or huge pages")]
    UserDevicesRequireSharedMemory,
    /// VSOCK Context Identifier has a special meaning, unsuitable for a VM.
    #[error("{0} is a special VSOCK CID")]
    VsockSpecialCid(u32),
    /// Memory zone is reused across NUMA nodes
    #[error("Memory zone: {0} belongs to multiple NUMA nodes: {1} and {2}")]
    MemoryZoneReused(String, u32, u32),
    /// Invalid number of PCI segments
    #[error("Number of PCI segments ({0}) not in range of 1 to {MAX_NUM_PCI_SEGMENTS}")]
    InvalidNumPciSegments(u16),
    /// Invalid PCI segment id
    #[error("Invalid PCI segment id: {0}")]
    InvalidPciSegment(u16),
    /// Invalid PCI segment aperture weight
    #[error("Invalid PCI segment aperture weight: {0}")]
    InvalidPciSegmentApertureWeight(u32),
    /// Invalid VFIO excluded-mmap BAR index
    #[error("Invalid VFIO excluded-mmap BAR index: {0}")]
    InvalidDeviceExcludeMmapBar(u64),
    /// Invalid IOMMU address width in bits
    #[error(
        "IOMMU address width in bits ({0}) should be less than or equal to {MAX_IOMMU_ADDRESS_WIDTH_BITS}"
    )]
    InvalidIommuAddressWidthBits(u8),
    /// Balloon too big
    #[error("Balloon size ({0}) greater than RAM ({1})")]
    BalloonLargerThanRam(u64, u64),
    /// On a IOMMU segment but not behind IOMMU
    #[error("Device is on an IOMMU PCI segment ({0}) but not placed behind IOMMU")]
    OnIommuSegment(u16),
    /// GPUDirect clique requires P2P DMA
    #[error("Device with x_nv_gpudirect_clique requires vfio_p2p_dma=on")]
    GpuDirectCliqueRequiresP2pDma,
    // Identifier is not unique
    #[error("Identifier {0} is not unique")]
    IdentifierNotUnique(String),
    /// Invalid identifier
    #[error("Identifier {0} is not invalid")]
    InvalidIdentifier(String),
    /// Placing the device behind a virtual IOMMU is not supported
    #[error("Device does not support being placed behind IOMMU")]
    IommuNotSupported,
    /// Duplicated device path (device added twice)
    #[error("Duplicated device path: {0}")]
    DuplicateDevicePath(String),
    /// A DeviceConfig specified neither `path` nor `fd`.
    #[error("VFIO device config must specify `path=` or `fd=`")]
    VfioDeviceNeitherPathNorFd,
    /// A DeviceConfig specified both `path` and `fd`.
    #[error("VFIO device config must specify at most one of `path=` or `fd=`")]
    VfioDeviceBothPathAndFd,
    /// FD-based VFIO requires an externally-supplied iommufd FD so the
    /// cdev's bind state can survive across an in-process VM reboot.
    #[error("VFIO device `fd=` requires platform `iommufd_fd=<fd>`")]
    VfioFdRequiresIommufdFd,
    /// `iommufd_fd` was provided without also enabling the iommufd backend.
    #[error("Platform `iommufd_fd=<fd>` requires `iommufd=on`")]
    IommufdFdRequiresIommufd,
    /// Provided MTU is lower than what the VIRTIO specification expects
    #[error("Provided MTU {0} is lower than 1280 (expected by VIRTIO specification)")]
    InvalidMtu(u16),
    /// PCI segment is reused across NUMA nodes
    #[error("PCI segment: {0} belongs to multiple NUMA nodes {1} and {2}")]
    PciSegmentReused(u16, u32, u32),
    /// Default PCI segment is assigned to NUMA node other than 0.
    #[error("Default PCI segment assigned to non-zero NUMA node {0}")]
    DefaultPciSegmentInvalidNode(u32),
    /// Invalid rate-limiter group
    #[error("Invalid rate-limiter group")]
    InvalidRateLimiterGroup,
    /// Rate limiting is not supported with vhost-user
    #[error("Rate limiting is not supported with vhost-user")]
    VhostUserRateLimiterNotSupported,
    /// The specified I/O port was invalid. It should be provided in hex, such as `0xe9`.
    #[cfg(target_arch = "x86_64")]
    #[error("The IO port was not properly provided in hex or a `0x` prefix is missing: {0}")]
    InvalidIoPortHex(String),
    #[cfg(feature = "sev_snp")]
    #[error("Invalid host data format")]
    InvalidHostData,
    #[cfg(feature = "sev_snp")]
    #[error("SEV-SNP requires an IGVM payload (--payload igvm=<path>)")]
    SevSnpRequiresIgvm,
    /// Restore expects all net ids that have fds
    #[error("Net id {0} is associated with FDs and is required")]
    RestoreMissingRequiredNetId(String),
    /// Number of FDs passed during Restore are incorrect to the NetConfig
    #[error("Number of Net FDs passed for '{0}' during Restore: {1}. Expected: {2}")]
    RestoreNetFdCountMismatch(String, usize, usize),
    /// Prefault cannot be combined with on-demand restore
    #[error("'prefault' cannot be combined with 'memory_restore_mode=ondemand'")]
    InvalidRestorePrefaultWithOnDemand,
    /// Path provided in landlock-rules doesn't exist
    #[error("Path {0:?} provided in landlock-rules does not exist")]
    LandlockPathDoesNotExist(PathBuf),
    /// Access provided in landlock-rules in invalid
    #[error("Invalid landlock access: {0}")]
    InvalidLandlockAccess(String),
    /// Invalid block device serial length
    #[error("Block device serial length ({0}) exceeds maximum allowed length ({1})")]
    InvalidSerialLength(usize, usize),
    #[cfg(feature = "ivshmem")]
    /// Invalid Ivshmem input size
    #[error("Invalid ivshmem input size: {0}")]
    InvalidIvshmemInputSize(u64),
    #[cfg(feature = "ivshmem")]
    /// Invalid Ivshmem backend file size
    #[error("Invalid ivshmem backend file size: {0}")]
    InvalidIvshmemSize(u64),
    #[cfg(feature = "ivshmem")]
    /// Invalid Ivshmem backend file path
    #[error("Invalid ivshmem backend file path")]
    InvalidIvshmemPath,
    #[error("Payload configuration is not bootable")]
    PayloadError(#[from] PayloadConfigError),
    #[error("Mask provided without an IP")]
    MaskProvidedWithoutIp,
    #[error("IP provided without a mask")]
    IpProvidedWithoutMask,
    /// Invalid NUMA Configuration
    #[error("Invalid NUMA configuration: {0}")]
    InvalidNumaConfig(String),
    /// The supplied PCI ID was greater then the max. supported number
    /// of devices per Bus
    #[error("Given PCI device ID ({0}) is out of the supported range of 0..{NUM_DEVICE_IDS}")]
    InvalidPciDeviceId(u8),
    /// The supplied PCI ID is reserved
    #[error("Given PCI device ID ({0}) is reserved")]
    ReservedPciDeviceId(u8),
    /// Invalid to set both 'mergeable' and 'shared' for memory
    #[error("Invalid to set both 'mergeable' and 'shared' for memory")]
    InvalidSharedMemoryWithMergeable,
}

type ValidationResult<T> = result::Result<T, ValidationError>;

pub fn add_to_config<T>(items: &mut Option<Vec<T>>, item: T) {
    if let Some(items) = items {
        items.push(item);
    } else {
        *items = Some(vec![item]);
    }
}

/// Check that the PCI device supplied is neither out of range nor does
/// it use any reserved device ID.
fn validate_pci_device_id(device_id: u8) -> ValidationResult<()> {
    if device_id >= pci::NUM_DEVICE_IDS {
        // Check the given ID is not out of range
        return Err(ValidationError::InvalidPciDeviceId(device_id));
    } else if device_id == pci::PCI_ROOT_DEVICE_ID {
        // Check the ID isn't any reserved one. Currently, only the device ID
        // for the root device is reserved.
        return Err(ValidationError::ReservedPciDeviceId(device_id));
    }

    Ok(())
}

/// Reject a virtio queue size that is not a power of 2 as required by the
/// spec. The `u16` type already handles the maximum queue size.
fn validate_queue_size(queue_size: u16) -> ValidationResult<()> {
    if !queue_size.is_power_of_two() {
        return Err(ValidationError::InvalidQueueSize(queue_size));
    }

    Ok(())
}

pub type Result<T> = result::Result<T, Error>;

pub struct VmParams<'a> {
    pub cpus: &'a str,
    pub memory: &'a str,
    pub memory_zones: Option<Vec<&'a str>>,
    pub firmware: Option<&'a str>,
    pub kernel: Option<&'a str>,
    pub initramfs: Option<&'a str>,
    pub cmdline: Option<&'a str>,
    pub rate_limit_groups: Option<Vec<&'a str>>,
    pub disks: Option<Vec<&'a str>>,
    pub net: Option<Vec<&'a str>>,
    pub rng: &'a str,
    pub balloon: Option<&'a str>,
    pub fs: Option<Vec<&'a str>>,
    pub generic_vhost_user: Option<Vec<&'a str>>,
    pub pmem: Option<Vec<&'a str>>,
    pub serial: &'a str,
    pub console: &'a str,
    #[cfg(target_arch = "x86_64")]
    pub debug_console: &'a str,
    pub devices: Option<Vec<&'a str>>,
    pub user_devices: Option<Vec<&'a str>>,
    pub vdpa: Option<Vec<&'a str>>,
    pub vsock: Option<&'a str>,
    #[cfg(feature = "pvmemcontrol")]
    pub pvmemcontrol: bool,
    pub pvpanic: bool,
    pub numa: Option<Vec<&'a str>>,
    pub watchdog: bool,
    pub rtc: Option<&'a str>,
    #[cfg(feature = "guest_debug")]
    pub gdb: bool,
    pub pci_segments: Option<Vec<&'a str>>,
    pub platform: Option<&'a str>,
    pub tpm: Option<&'a str>,
    #[cfg(feature = "igvm")]
    pub igvm: Option<&'a str>,
    #[cfg(feature = "sev_snp")]
    pub host_data: Option<&'a str>,
    pub landlock_enable: bool,
    pub landlock_rules: Option<Vec<&'a str>>,
    #[cfg(feature = "fw_cfg")]
    pub fw_cfg_config: Option<&'a str>,
    #[cfg(feature = "ivshmem")]
    pub ivshmem: Option<&'a str>,
}

impl<'a> VmParams<'a> {
    pub fn from_arg_matches(args: &'a ArgMatches) -> Self {
        // These .unwrap()s cannot fail as there is a default value defined
        let cpus = args.get_one::<String>("cpus").unwrap();
        let memory = args.get_one::<String>("memory").unwrap();
        let memory_zones: Option<Vec<&str>> = args
            .get_many::<String>("memory-zone")
            .map(|x| x.map(|y| y as &str).collect());
        let rng = args.get_one::<String>("rng").unwrap();
        let serial = args.get_one::<String>("serial").unwrap();
        let firmware = args.get_one::<String>("firmware").map(|x| x as &str);
        let kernel = args.get_one::<String>("kernel").map(|x| x as &str);
        let initramfs = args.get_one::<String>("initramfs").map(|x| x as &str);
        let cmdline = args.get_one::<String>("cmdline").map(|x| x as &str);
        let rate_limit_groups: Option<Vec<&str>> = args
            .get_many::<String>("rate-limit-group")
            .map(|x| x.map(|y| y as &str).collect());
        let disks: Option<Vec<&str>> = args
            .get_many::<String>("disk")
            .map(|x| x.map(|y| y as &str).collect());
        let net: Option<Vec<&str>> = args
            .get_many::<String>("net")
            .map(|x| x.map(|y| y as &str).collect());
        let console = args.get_one::<String>("console").unwrap();
        #[cfg(target_arch = "x86_64")]
        let debug_console = args.get_one::<String>("debug-console").unwrap().as_str();
        let balloon = args.get_one::<String>("balloon").map(|x| x as &str);
        let fs: Option<Vec<&str>> = args
            .get_many::<String>("fs")
            .map(|x| x.map(|y| y as &str).collect());
        let generic_vhost_user: Option<Vec<&str>> = args
            .get_many::<String>("generic-vhost-user")
            .map(|x| x.map(|y| y as &str).collect());
        let pmem: Option<Vec<&str>> = args
            .get_many::<String>("pmem")
            .map(|x| x.map(|y| y as &str).collect());
        let devices: Option<Vec<&str>> = args
            .get_many::<String>("device")
            .map(|x| x.map(|y| y as &str).collect());
        let user_devices: Option<Vec<&str>> = args
            .get_many::<String>("user-device")
            .map(|x| x.map(|y| y as &str).collect());
        let vdpa: Option<Vec<&str>> = args
            .get_many::<String>("vdpa")
            .map(|x| x.map(|y| y as &str).collect());
        let vsock: Option<&str> = args.get_one::<String>("vsock").map(|x| x as &str);
        #[cfg(feature = "pvmemcontrol")]
        let pvmemcontrol = args.get_flag("pvmemcontrol");
        let pvpanic = args.get_flag("pvpanic");
        let numa: Option<Vec<&str>> = args
            .get_many::<String>("numa")
            .map(|x| x.map(|y| y as &str).collect());
        let watchdog = args.get_flag("watchdog");
        let rtc: Option<&str> = args.get_one::<String>("rtc").map(|x| x as &str);
        let pci_segments: Option<Vec<&str>> = args
            .get_many::<String>("pci-segment")
            .map(|x| x.map(|y| y as &str).collect());
        let platform = args.get_one::<String>("platform").map(|x| x as &str);
        #[cfg(feature = "guest_debug")]
        let gdb = args.contains_id("gdb");
        let tpm: Option<&str> = args.get_one::<String>("tpm").map(|x| x as &str);
        #[cfg(feature = "igvm")]
        let igvm = args.get_one::<String>("igvm").map(|x| x as &str);
        #[cfg(feature = "sev_snp")]
        let host_data = args.get_one::<String>("host-data").map(|x| x as &str);
        let landlock_enable = args.get_flag("landlock");
        let landlock_rules: Option<Vec<&str>> = args
            .get_many::<String>("landlock-rules")
            .map(|x| x.map(|y| y as &str).collect());
        #[cfg(feature = "fw_cfg")]
        let fw_cfg_config: Option<&str> =
            args.get_one::<String>("fw-cfg-config").map(|x| x as &str);
        #[cfg(feature = "ivshmem")]
        let ivshmem: Option<&str> = args.get_one::<String>("ivshmem").map(|x| x as &str);
        VmParams {
            cpus,
            memory,
            memory_zones,
            firmware,
            kernel,
            initramfs,
            cmdline,
            rate_limit_groups,
            disks,
            net,
            rng,
            balloon,
            fs,
            generic_vhost_user,
            pmem,
            serial,
            console,
            #[cfg(target_arch = "x86_64")]
            debug_console,
            devices,
            user_devices,
            vdpa,
            vsock,
            #[cfg(feature = "pvmemcontrol")]
            pvmemcontrol,
            pvpanic,
            numa,
            watchdog,
            rtc,
            #[cfg(feature = "guest_debug")]
            gdb,
            pci_segments,
            platform,
            tpm,
            #[cfg(feature = "igvm")]
            igvm,
            #[cfg(feature = "sev_snp")]
            host_data,
            landlock_enable,
            landlock_rules,
            #[cfg(feature = "fw_cfg")]
            fw_cfg_config,
            #[cfg(feature = "ivshmem")]
            ivshmem,
        }
    }
}

#[derive(Debug)]
pub enum ParseHotplugMethodError {
    InvalidValue(String),
}

impl FromStr for HotplugMethod {
    type Err = ParseHotplugMethodError;

    fn from_str(s: &str) -> result::Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "acpi" => Ok(HotplugMethod::Acpi),
            "virtio-mem" => Ok(HotplugMethod::VirtioMem),
            _ => Err(ParseHotplugMethodError::InvalidValue(s.to_owned())),
        }
    }
}

pub enum ParseCoreSchedulingError {
    InvalidValue(String),
}

impl FromStr for CoreScheduling {
    type Err = ParseCoreSchedulingError;

    fn from_str(s: &str) -> result::Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "vm" => Ok(CoreScheduling::Vm),
            "vcpu" => Ok(CoreScheduling::Vcpu),
            "off" => Ok(CoreScheduling::Off),
            _ => Err(ParseCoreSchedulingError::InvalidValue(s.to_owned())),
        }
    }
}

pub enum CpuTopologyParseError {
    InvalidValue(String),
}

impl FromStr for CpuTopology {
    type Err = CpuTopologyParseError;

    fn from_str(s: &str) -> result::Result<Self, Self::Err> {
        let parts: Vec<&str> = s.split(':').collect();

        if parts.len() != 4 {
            return Err(Self::Err::InvalidValue(s.to_owned()));
        }

        let t = CpuTopology {
            threads_per_core: parts[0]
                .parse()
                .map_err(|_| Self::Err::InvalidValue(s.to_owned()))?,
            cores_per_die: parts[1]
                .parse()
                .map_err(|_| Self::Err::InvalidValue(s.to_owned()))?,
            dies_per_package: parts[2]
                .parse()
                .map_err(|_| Self::Err::InvalidValue(s.to_owned()))?,
            packages: parts[3]
                .parse()
                .map_err(|_| Self::Err::InvalidValue(s.to_owned()))?,
        };

        Ok(t)
    }
}

impl CpusConfig {
    pub fn parse(cpus: &str) -> Result<Self> {
        let mut parser = OptionParser::new();
        parser
            .add("boot")
            .add("max")
            .add("topology")
            .add("kvm_hyperv")
            .add("max_phys_bits")
            .add("affinity")
            .add("features")
            .add("nested")
            .add("core_scheduling")
            .add("profile");
        parser.parse(cpus).map_err(Error::ParseCpus)?;

        let boot_vcpus: u32 = parser
            .convert("boot")
            .map_err(Error::ParseCpus)?
            .unwrap_or(DEFAULT_VCPUS);
        let max_vcpus: u32 = parser
            .convert("max")
            .map_err(Error::ParseCpus)?
            .unwrap_or(boot_vcpus);
        let topology = parser.convert("topology").map_err(Error::ParseCpus)?;
        let kvm_hyperv = parser
            .convert::<Toggle>("kvm_hyperv")
            .map_err(Error::ParseCpus)?
            .unwrap_or(Toggle(false))
            .0;
        let max_phys_bits = parser
            .convert::<u8>("max_phys_bits")
            .map_err(Error::ParseCpus)?
            .unwrap_or(DEFAULT_MAX_PHYS_BITS);
        let affinity = parser
            .convert::<Tuple<u32, Vec<usize>>>("affinity")
            .map_err(Error::ParseCpus)?
            .map(|v| {
                v.0.iter()
                    .map(|(e1, e2)| CpuAffinity {
                        vcpu: *e1,
                        host_cpus: e2.clone().into_boxed_slice(),
                    })
                    .collect()
            });

        let profile = parser
            .convert::<CpuProfile>("profile")
            .map_err(Error::ParseCpus)?
            .unwrap_or_default();

        let features_list = parser
            .convert::<StringList>("features")
            .map_err(Error::ParseCpus)?
            .unwrap_or_default();

        #[allow(unused_mut)]
        let mut features = CpuFeatures::default();
        {
            #[cfg(target_arch = "x86_64")]
            for feature in features_list.0 {
                match feature.as_str() {
                    "amx" => features.amx = true,
                    _ => return Err(Error::InvalidCpuFeatures(feature)),
                }
            }

            #[cfg(not(target_arch = "x86_64"))]
            if let Some(feature) = features_list.0.into_iter().next() {
                return Err(Error::InvalidCpuFeatures(feature));
            }
        }

        let nested = parser
            .convert::<Toggle>("nested")
            .map_err(Error::ParseCpus)?
            .is_none_or(|toggle| toggle.0);

        let core_scheduling = parser
            .convert("core_scheduling")
            .map_err(Error::ParseCpus)?
            .unwrap_or(CoreScheduling::Vm);

        Ok(CpusConfig {
            boot_vcpus,
            max_vcpus,
            topology,
            kvm_hyperv,
            max_phys_bits,
            affinity,
            features,
            nested,
            core_scheduling,
            profile,
        })
    }
}

impl PciSegmentConfig {
    pub const SYNTAX: &'static str = "PCI Segment parameters \
         \"pci_segment=<segment_id>,mmio32_aperture_weight=<scale>,mmio64_aperture_weight=<scale>\"";

    pub fn parse(disk: &str) -> Result<Self> {
        let mut parser = OptionParser::new();
        parser
            .add("mmio32_aperture_weight")
            .add("mmio64_aperture_weight")
            .add("pci_segment");
        parser.parse(disk).map_err(Error::ParsePciSegment)?;

        let pci_segment = parser
            .convert("pci_segment")
            .map_err(Error::ParsePciSegment)?
            .unwrap_or_default();
        let mmio32_aperture_weight = parser
            .convert("mmio32_aperture_weight")
            .map_err(Error::ParsePciSegment)?
            .unwrap_or(DEFAULT_PCI_SEGMENT_APERTURE_WEIGHT);
        let mmio64_aperture_weight = parser
            .convert("mmio64_aperture_weight")
            .map_err(Error::ParsePciSegment)?
            .unwrap_or(DEFAULT_PCI_SEGMENT_APERTURE_WEIGHT);

        Ok(PciSegmentConfig {
            pci_segment,
            mmio32_aperture_weight,
            mmio64_aperture_weight,
        })
    }

    pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> {
        let num_pci_segments = match &vm_config.platform {
            Some(platform_config) => platform_config.num_pci_segments,
            None => 1,
        };

        if self.pci_segment >= num_pci_segments {
            return Err(ValidationError::InvalidPciSegment(self.pci_segment));
        }

        if self.mmio32_aperture_weight == 0 {
            return Err(ValidationError::InvalidPciSegmentApertureWeight(
                self.mmio32_aperture_weight,
            ));
        }

        if self.mmio64_aperture_weight == 0 {
            return Err(ValidationError::InvalidPciSegmentApertureWeight(
                self.mmio64_aperture_weight,
            ));
        }

        Ok(())
    }
}

impl PlatformConfig {
    pub fn syntax() -> &'static str {
        static SYNTAX: LazyLock<String> = LazyLock::new(|| {
            let mut syntax = "Platform configuration parameters \
            \"num_pci_segments=<num_pci_segments>,iommu_segments=<list_of_segments>,\
            iommu_address_width=<bits>,iommufd=on|off,iommufd_fd=<fd>,vfio_p2p_dma=on|off,\
            system_manufacturer=<dmi_system_manufacturer>,\
            system_product_name=<dmi_system_product_name>,system_version=<dmi_system_version>,\
            system_serial_number=<dmi_system_serial_number>,system_uuid=<dmi_system_uuid>,\
            system_sku_number=<dmi_system_sku_number>,system_family=<dmi_system_family>,\
            oem_strings=<list_of_strings>,chassis_asset_tag=<dmi_chassis_asset_tag>"
                .to_string();

            if cfg!(feature = "tdx") {
                syntax.push_str(",tdx=on|off");
            }

            if cfg!(feature = "sev_snp") {
                syntax.push_str(",sev_snp=on|off");
            }

            syntax.push('"');

            syntax
        });

        &SYNTAX
    }

    pub fn parse(platform: &str) -> Result<Self> {
        struct StringField {
            key: &'static str,
            apply: fn(&mut PlatformConfig, String),
        }

        const SMBIOS_STRING_FIELDS: &[StringField] = &[
            StringField {
                key: "system_manufacturer",
                apply: |config, value| config.system_manufacturer = Some(value),
            },
            StringField {
                key: "system_product_name",
                apply: |config, value| config.system_product_name = Some(value),
            },
            StringField {
                key: "system_version",
                apply: |config, value| config.system_version = Some(value),
            },
            StringField {
                key: "system_serial_number",
                apply: |config, value| config.system_serial_number = Some(value),
            },
            StringField {
                key: "system_uuid",
                apply: |config, value| config.system_uuid = Some(value),
            },
            StringField {
                key: "system_sku_number",
                apply: |config, value| config.system_sku_number = Some(value),
            },
            StringField {
                key: "system_family",
                apply: |config, value| config.system_family = Some(value),
            },
            StringField {
                key: "chassis_asset_tag",
                apply: |config, value| config.chassis_asset_tag = Some(value),
            },
        ];

        let mut parser = OptionParser::new();
        parser
            .add("num_pci_segments")
            .add("iommu_segments")
            .add("iommu_address_width")
            .add("serial_number")
            .add("uuid")
            .add("oem_strings")
            .add("iommufd")
            .add("iommufd_fd")
            .add("vfio_p2p_dma");
        for field in SMBIOS_STRING_FIELDS {
            parser.add(field.key);
        }
        #[cfg(feature = "tdx")]
        parser.add("tdx");
        #[cfg(feature = "sev_snp")]
        parser.add("sev_snp");
        parser.parse(platform).map_err(Error::ParsePlatform)?;

        let num_pci_segments: u16 = parser
            .convert("num_pci_segments")
            .map_err(Error::ParsePlatform)?
            .unwrap_or(DEFAULT_NUM_PCI_SEGMENTS);
        let iommu_segments = parser
            .convert::<IntegerList>("iommu_segments")
            .map_err(Error::ParsePlatform)?
            .map(|v| v.0.iter().map(|e| *e as u16).collect());
        let iommu_address_width_bits: u8 = parser
            .convert("iommu_address_width")
            .map_err(Error::ParsePlatform)?
            .unwrap_or(MAX_IOMMU_ADDRESS_WIDTH_BITS);
        let oem_strings = parser
            .convert::<StringList>("oem_strings")
            .map_err(Error::ParsePlatform)?
            .map(|v| v.0.into_boxed_slice());
        let iommufd_fd = parser
            .convert::<i32>("iommufd_fd")
            .map_err(Error::ParsePlatform)?;
        // `iommufd_fd=<n>` implies `iommufd=on` unless the user explicitly set the value.
        let iommufd = parser
            .convert::<Toggle>("iommufd")
            .map_err(Error::ParsePlatform)?
            .map_or(iommufd_fd.is_some(), |Toggle(v)| v);
        let vfio_p2p_dma = parser
            .convert::<Toggle>("vfio_p2p_dma")
            .map_err(Error::ParsePlatform)?
            .unwrap_or(Toggle(true))
            .0;
        #[cfg(feature = "tdx")]
        let tdx = parser
            .convert::<Toggle>("tdx")
            .map_err(Error::ParsePlatform)?
            .unwrap_or(Toggle(false))
            .0;
        #[cfg(feature = "sev_snp")]
        let sev_snp = parser
            .convert::<Toggle>("sev_snp")
            .map_err(Error::ParsePlatform)?
            .unwrap_or(Toggle(false))
            .0;

        let mut platform_config = PlatformConfig {
            num_pci_segments,
            iommu_segments,
            iommu_address_width_bits,
            system_serial_number: None,
            system_uuid: None,
            oem_strings,
            system_manufacturer: None,
            system_product_name: None,
            system_version: None,
            system_family: None,
            system_sku_number: None,
            chassis_asset_tag: None,
            iommufd,
            iommufd_fd,
            #[cfg(feature = "tdx")]
            tdx,
            #[cfg(feature = "sev_snp")]
            sev_snp,
            vfio_p2p_dma,
        };

        for field in SMBIOS_STRING_FIELDS {
            if let Some(value) = parser
                .convert::<String>(field.key)
                .map_err(Error::ParsePlatform)?
            {
                (field.apply)(&mut platform_config, value);
            }
        }

        let legacy_serial_number = parser
            .convert::<String>("serial_number")
            .map_err(Error::ParsePlatform)?;
        if legacy_serial_number.is_some() {
            warn!("'serial_number' in --platform is deprecated; use 'system_serial_number'.");
        }
        platform_config.system_serial_number = platform_config
            .system_serial_number
            .or(legacy_serial_number);

        let legacy_uuid = parser
            .convert::<String>("uuid")
            .map_err(Error::ParsePlatform)?;
        if legacy_uuid.is_some() {
            warn!("'uuid' in --platform is deprecated; use 'system_uuid'.");
        }
        platform_config.system_uuid = platform_config.system_uuid.or(legacy_uuid);

        Ok(platform_config)
    }

    pub fn validate(&self) -> ValidationResult<()> {
        if self.num_pci_segments == 0 || self.num_pci_segments > MAX_NUM_PCI_SEGMENTS {
            return Err(ValidationError::InvalidNumPciSegments(
                self.num_pci_segments,
            ));
        }

        if let Some(iommu_segments) = &self.iommu_segments {
            for segment in iommu_segments {
                if *segment >= self.num_pci_segments {
                    return Err(ValidationError::InvalidPciSegment(*segment));
                }
            }
        }

        if self.iommu_address_width_bits > MAX_IOMMU_ADDRESS_WIDTH_BITS {
            return Err(ValidationError::InvalidIommuAddressWidthBits(
                self.iommu_address_width_bits,
            ));
        }

        if self.iommufd_fd.is_some() && !self.iommufd {
            return Err(ValidationError::IommufdFdRequiresIommufd);
        }

        Ok(())
    }
}

impl MemoryConfig {
    #[expect(clippy::needless_pass_by_value)]
    pub fn parse(memory: &str, memory_zones: Option<Vec<&str>>) -> Result<Self> {
        let mut parser = OptionParser::new();
        parser
            .add("size")
            .add("file")
            .add("mergeable")
            .add("hotplug_method")
            .add("hotplug_size")
            .add("hotplugged_size")
            .add("shared")
            .add("hugepages")
            .add("hugepage_size")
            .add("prefault")
            .add("reserve")
            .add("thp");
        parser.parse(memory).map_err(Error::ParseMemory)?;

        let size = parser
            .convert::<ByteSized>("size")
            .map_err(Error::ParseMemory)?
            .unwrap_or(ByteSized(DEFAULT_MEMORY_MB << 20))
            .0;
        let mergeable = parser
            .convert::<Toggle>("mergeable")
            .map_err(Error::ParseMemory)?
            .unwrap_or(Toggle(false))
            .0;
        let hotplug_method = parser
            .convert("hotplug_method")
            .map_err(Error::ParseMemory)?
            .unwrap_or_default();
        let hotplug_size = parser
            .convert::<ByteSized>("hotplug_size")
            .map_err(Error::ParseMemory)?
            .map(|v| v.0);
        let hotplugged_size = parser
            .convert::<ByteSized>("hotplugged_size")
            .map_err(Error::ParseMemory)?
            .map(|v| v.0);
        let shared = parser
            .convert::<Toggle>("shared")
            .map_err(Error::ParseMemory)?
            .unwrap_or(Toggle(false))
            .0;
        let hugepages = parser
            .convert::<Toggle>("hugepages")
            .map_err(Error::ParseMemory)?
            .unwrap_or(Toggle(false))
            .0;
        let hugepage_size = parser
            .convert::<ByteSized>("hugepage_size")
            .map_err(Error::ParseMemory)?
            .map(|v| v.0);
        let prefault = parser
            .convert::<Toggle>("prefault")
            .map_err(Error::ParseMemory)?
            .unwrap_or(Toggle(false))
            .0;
        let reserve = parser
            .convert::<Toggle>("reserve")
            .map_err(Error::ParseMemory)?
            .unwrap_or(Toggle(false))
            .0;
        let thp = parser
            .convert::<Toggle>("thp")
            .map_err(Error::ParseMemory)?
            .unwrap_or(Toggle(true))
            .0;

        let zones: Option<Vec<MemoryZoneConfig>> = if let Some(memory_zones) = &memory_zones {
            let mut zones = Vec::new();
            for memory_zone in memory_zones.iter() {
                let mut parser = OptionParser::new();
                parser
                    .add("id")
                    .add("size")
                    .add("file")
                    .add("shared")
                    .add("hugepages")
                    .add("hugepage_size")
                    .add("host_numa_node")
                    .add("hotplug_size")
                    .add("hotplugged_size")
                    .add("prefault")
                    .add("reserve")
                    .add("mergeable");
                parser.parse(memory_zone).map_err(Error::ParseMemoryZone)?;

                let id = parser.get("id").ok_or(Error::ParseMemoryZoneIdMissing)?;
                let size = parser
                    .convert::<ByteSized>("size")
                    .map_err(Error::ParseMemoryZone)?
                    .unwrap_or(ByteSized(DEFAULT_MEMORY_MB << 20))
                    .0;
                let file = parser.get("file").map(PathBuf::from);
                let shared = parser
                    .convert::<Toggle>("shared")
                    .map_err(Error::ParseMemoryZone)?
                    .unwrap_or(Toggle(false))
                    .0;
                let hugepages = parser
                    .convert::<Toggle>("hugepages")
                    .map_err(Error::ParseMemoryZone)?
                    .unwrap_or(Toggle(false))
                    .0;
                let hugepage_size = parser
                    .convert::<ByteSized>("hugepage_size")
                    .map_err(Error::ParseMemoryZone)?
                    .map(|v| v.0);

                let host_numa_node = parser
                    .convert::<u32>("host_numa_node")
                    .map_err(Error::ParseMemoryZone)?;
                let hotplug_size = parser
                    .convert::<ByteSized>("hotplug_size")
                    .map_err(Error::ParseMemoryZone)?
                    .map(|v| v.0);
                let hotplugged_size = parser
                    .convert::<ByteSized>("hotplugged_size")
                    .map_err(Error::ParseMemoryZone)?
                    .map(|v| v.0);
                let prefault = parser
                    .convert::<Toggle>("prefault")
                    .map_err(Error::ParseMemoryZone)?
                    .unwrap_or(Toggle(false))
                    .0;
                let reserve = parser
                    .convert::<Toggle>("reserve")
                    .map_err(Error::ParseMemoryZone)?
                    .unwrap_or(Toggle(false))
                    .0;
                let mergeable = parser
                    .convert::<Toggle>("mergeable")
                    .map_err(Error::ParseMemoryZone)?
                    .unwrap_or(Toggle(mergeable))
                    .0;

                zones.push(MemoryZoneConfig {
                    id,
                    size,
                    file,
                    shared,
                    hugepages,
                    hugepage_size,
                    host_numa_node,
                    hotplug_size,
                    hotplugged_size,
                    prefault,
                    reserve,
                    mergeable,
                });
            }
            Some(zones)
        } else {
            None
        };

        Ok(MemoryConfig {
            size,
            mergeable,
            hotplug_method,
            hotplug_size,
            hotplugged_size,
            shared,
            hugepages,
            hugepage_size,
            prefault,
            reserve,
            zones,
            thp,
        })
    }

    pub fn total_size(&self) -> u64 {
        self.size
            + self
                .zones
                .iter()
                .flatten()
                .map(|zone| zone.size)
                .sum::<u64>()
            + self.hotplugged_size()
    }

    pub fn hotplugged_size(&self) -> u64 {
        self.hotplugged_size.unwrap_or(0)
            + self
                .zones
                .iter()
                .flatten()
                .filter_map(|zone| zone.hotplugged_size)
                .sum::<u64>()
    }
}

impl RateLimiterGroupConfig {
    pub const SYNTAX: &'static str = "Rate Limit Group parameters \
        \"bw_size=<bytes>,bw_one_time_burst=<bytes>,bw_refill_time=<ms>,\
        ops_size=<io_ops>,ops_one_time_burst=<io_ops>,ops_refill_time=<ms>,\
        id=<device_id>\"";

    pub fn parse(rate_limit_group: &str) -> Result<Self> {
        let mut parser = OptionParser::new();
        parser
            .add("bw_size")
            .add("bw_one_time_burst")
            .add("bw_refill_time")
            .add("ops_size")
            .add("ops_one_time_burst")
            .add("ops_refill_time")
            .add("id");
        parser
            .parse(rate_limit_group)
            .map_err(Error::ParseRateLimiterGroup)?;

        let id = parser.get("id").unwrap_or_default();
        let bw_size = parser
            .convert("bw_size")
            .map_err(Error::ParseRateLimiterGroup)?
            .unwrap_or_default();
        let bw_one_time_burst = parser
            .convert("bw_one_time_burst")
            .map_err(Error::ParseRateLimiterGroup)?
            .unwrap_or_default();
        let bw_refill_time = parser
            .convert("bw_refill_time")
            .map_err(Error::ParseRateLimiterGroup)?
            .unwrap_or_default();
        let ops_size = parser
            .convert("ops_size")
            .map_err(Error::ParseRateLimiterGroup)?
            .unwrap_or_default();
        let ops_one_time_burst = parser
            .convert("ops_one_time_burst")
            .map_err(Error::ParseRateLimiterGroup)?
            .unwrap_or_default();
        let ops_refill_time = parser
            .convert("ops_refill_time")
            .map_err(Error::ParseRateLimiterGroup)?
            .unwrap_or_default();

        let bw_tb_config = if bw_size != 0 && bw_refill_time != 0 {
            Some(TokenBucketConfig {
                size: bw_size,
                one_time_burst: Some(bw_one_time_burst),
                refill_time: bw_refill_time,
            })
        } else {
            None
        };
        let ops_tb_config = if ops_size != 0 && ops_refill_time != 0 {
            Some(TokenBucketConfig {
                size: ops_size,
                one_time_burst: Some(ops_one_time_burst),
                refill_time: ops_refill_time,
            })
        } else {
            None
        };

        Ok(RateLimiterGroupConfig {
            id,
            rate_limiter_config: RateLimiterConfig {
                bandwidth: bw_tb_config,
                ops: ops_tb_config,
            },
        })
    }

    pub fn validate(&self, _vm_config: &VmConfig) -> ValidationResult<()> {
        if self.rate_limiter_config.bandwidth.is_none() && self.rate_limiter_config.ops.is_none() {
            return Err(ValidationError::InvalidRateLimiterGroup);
        }

        if self.id.is_empty() {
            return Err(ValidationError::InvalidRateLimiterGroup);
        }

        Ok(())
    }
}

impl PciDeviceCommonConfig {
    const OPTIONS: &[&str] = &["id", "pci_segment", "pci_device_id"];
    const OPTIONS_IOMMU: &[&str] = &["id", "iommu", "pci_segment", "pci_device_id"];

    pub fn parse(input: &str) -> Result<Self> {
        let mut parser = OptionParser::new();

        parser.add_all(Self::OPTIONS_IOMMU);

        parser
            .parse_subset(input)
            .map_err(Error::ParsePciDeviceCommonConfig)?;

        let id = parser.get("id");
        let iommu = parser
            .convert::<Toggle>("iommu")
            .map_err(Error::ParsePciDeviceCommonConfig)?
            .unwrap_or(Toggle(false))
            .0;
        let pci_segment = parser
            .convert("pci_segment")
            .map_err(Error::ParsePciDeviceCommonConfig)?
            .unwrap_or_default();
        let pci_device_id = parser
            .convert::<u8>("pci_device_id")
            .map_err(Error::ParsePciDeviceCommonConfig)?;

        Ok(Self {
            id,
            iommu,
            pci_segment,
            pci_device_id,
        })
    }

    pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> {
        let num_pci_segments = vm_config
            .platform
            .as_ref()
            .map_or(DEFAULT_NUM_PCI_SEGMENTS, |platform_config| {
                platform_config.num_pci_segments
            });

        if self.pci_segment >= num_pci_segments {
            return Err(ValidationError::InvalidPciSegment(self.pci_segment));
        }

        if let Some(platform_config) = vm_config.platform.as_ref()
            && let Some(iommu_segments) = platform_config.iommu_segments.as_ref()
            && iommu_segments.contains(&self.pci_segment)
            && !self.iommu
        {
            return Err(ValidationError::OnIommuSegment(self.pci_segment));
        }

        if let Some(device_id) = self.pci_device_id {
            validate_pci_device_id(device_id)?;
        }

        Ok(())
    }
}

impl DiskConfig {
    pub const SYNTAX: &'static str = "Disk parameters \
         \"path=<disk_image_path>,readonly=on|off,direct=on|off,iommu=on|off,\
         num_queues=<number_of_queues>,queue_size=<size_of_each_queue>,\
         vhost_user=on|off,socket=<vhost_user_socket_path>,\
         bw_size=<bytes>,bw_one_time_burst=<bytes>,bw_refill_time=<ms>,\
         ops_size=<io_ops>,ops_one_time_burst=<io_ops>,ops_refill_time=<ms>,\
         id=<device_id>,pci_segment=<segment_id>,pci_device_id=<pci_slot>,\
         rate_limit_group=<group_id>,\
         queue_affinity=<list_of_queue_indices_with_their_associated_cpuset>,\
         serial=<serial_number>,backing_files=on|off,sparse=on|off,\
         image_type=<raw,qcow2,vhd,vhdx>,lock_granularity=byte-range|full";

    pub fn parse(disk: &str) -> Result<Self> {
        let mut parser = OptionParser::new();
        parser
            .add("path")
            .add("readonly")
            .add("direct")
            .add("queue_size")
            .add("num_queues")
            .add("vhost_user")
            .add("socket")
            .add("bw_size")
            .add("bw_one_time_burst")
            .add("bw_refill_time")
            .add("ops_size")
            .add("ops_one_time_burst")
            .add("ops_refill_time")
            .add("_disable_io_uring")
            .add("_disable_aio")
            .add("serial")
            .add("rate_limit_group")
            .add("queue_affinity")
            .add("backing_files")
            .add("sparse")
            .add("image_type")
            .add("lock_granularity")
            .add_all(PciDeviceCommonConfig::OPTIONS_IOMMU);

        parser.parse(disk).map_err(Error::ParseDisk)?;

        let path = parser.get("path").map(PathBuf::from);
        let readonly = parser
            .convert::<Toggle>("readonly")
            .map_err(Error::ParseDisk)?
            .unwrap_or(Toggle(false))
            .0;
        let direct = parser
            .convert::<Toggle>("direct")
            .map_err(Error::ParseDisk)?
            .unwrap_or(Toggle(false))
            .0;
        let queue_size = parser
            .convert("queue_size")
            .map_err(Error::ParseDisk)?
            .unwrap_or_else(default_diskconfig_queue_size);
        let num_queues = parser
            .convert("num_queues")
            .map_err(Error::ParseDisk)?
            .unwrap_or_else(default_diskconfig_num_queues);
        let vhost_user = parser
            .convert::<Toggle>("vhost_user")
            .map_err(Error::ParseDisk)?
            .unwrap_or(Toggle(false))
            .0;
        let vhost_socket = parser.get("socket");
        let disable_io_uring = parser
            .convert::<Toggle>("_disable_io_uring")
            .map_err(Error::ParseDisk)?
            .unwrap_or(Toggle(false))
            .0;
        let disable_aio = parser
            .convert::<Toggle>("_disable_aio")
            .map_err(Error::ParseDisk)?
            .unwrap_or(Toggle(false))
            .0;
        let rate_limit_group = parser.get("rate_limit_group");
        let bw_size = parser
            .convert("bw_size")
            .map_err(Error::ParseDisk)?
            .unwrap_or_default();
        let bw_one_time_burst = parser
            .convert("bw_one_time_burst")
            .map_err(Error::ParseDisk)?
            .unwrap_or_default();
        let bw_refill_time = parser
            .convert("bw_refill_time")
            .map_err(Error::ParseDisk)?
            .unwrap_or_default();
        let ops_size = parser
            .convert("ops_size")
            .map_err(Error::ParseDisk)?
            .unwrap_or_default();
        let ops_one_time_burst = parser
            .convert("ops_one_time_burst")
            .map_err(Error::ParseDisk)?
            .unwrap_or_default();
        let ops_refill_time = parser
            .convert("ops_refill_time")
            .map_err(Error::ParseDisk)?
            .unwrap_or_default();
        let serial = parser.get("serial");
        let queue_affinity = parser
            .convert::<Tuple<u16, Vec<usize>>>("queue_affinity")
            .map_err(Error::ParseDisk)?
            .map(|v| {
                v.0.iter()
                    .map(|(e1, e2)| VirtQueueAffinity {
                        queue_index: *e1,
                        host_cpus: e2.clone().into_boxed_slice(),
                    })
                    .collect()
            });

        let backing_files = parser
            .convert::<Toggle>("backing_files")
            .map_err(Error::ParseDisk)?
            .unwrap_or(Toggle(false))
            .0;

        let image_type = if vhost_socket.is_none() {
            parser
                .convert::<ImageType>("image_type")
                .map_err(Error::ParseDisk)?
                .unwrap_or(ImageType::Unknown)
        } else {
            ImageType::Unknown
        };

        let lock_granularity = parser
            .convert::<LockGranularityChoice>("lock_granularity")
            .map_err(Error::ParseDisk)?
            .unwrap_or_default();

        let bw_tb_config = if bw_size != 0 && bw_refill_time != 0 {
            Some(TokenBucketConfig {
                size: bw_size,
                one_time_burst: Some(bw_one_time_burst),
                refill_time: bw_refill_time,
            })
        } else {
            None
        };
        let ops_tb_config = if ops_size != 0 && ops_refill_time != 0 {
            Some(TokenBucketConfig {
                size: ops_size,
                one_time_burst: Some(ops_one_time_burst),
                refill_time: ops_refill_time,
            })
        } else {
            None
        };
        let rate_limiter_config = if bw_tb_config.is_some() || ops_tb_config.is_some() {
            Some(RateLimiterConfig {
                bandwidth: bw_tb_config,
                ops: ops_tb_config,
            })
        } else {
            None
        };
        let sparse = parser
            .convert::<Toggle>("sparse")
            .map_err(Error::ParseDisk)?
            .unwrap_or_else(|| Toggle(default_diskconfig_sparse()))
            .0;

        let pci_common = PciDeviceCommonConfig::parse(disk)?;

        Ok(DiskConfig {
            pci_common,
            path,
            readonly,
            direct,
            num_queues,
            queue_size,
            vhost_user,
            vhost_socket,
            rate_limit_group,
            rate_limiter_config,
            disable_io_uring,
            disable_aio,
            serial,
            queue_affinity,
            backing_files,
            sparse,
            image_type,
            lock_granularity,
        })
    }

    pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> {
        self.pci_common.validate(vm_config)?;

        if self.num_queues > vm_config.cpus.boot_vcpus as usize {
            return Err(ValidationError::TooManyQueues(
                self.num_queues,
                vm_config.cpus.boot_vcpus as usize,
            ));
        }

        validate_queue_size(self.queue_size)?;

        if self.queue_size <= MINIMUM_BLOCK_QUEUE_SIZE {
            return Err(ValidationError::BlockQueueSizeTooSmall(self.queue_size));
        }

        if self.vhost_user && self.pci_common.iommu {
            return Err(ValidationError::IommuNotSupported);
        }

        if self.vhost_user && self.rate_limiter_config.is_some() {
            return Err(ValidationError::VhostUserRateLimiterNotSupported);
        }

        if self.vhost_user && self.rate_limit_group.is_some() {
            return Err(ValidationError::VhostUserRateLimiterNotSupported);
        }

        if self.rate_limiter_config.is_some() && self.rate_limit_group.is_some() {
            return Err(ValidationError::InvalidRateLimiterGroup);
        }

        // Check Block device serial length
        if let Some(ref serial) = self.serial
            && serial.len() > VIRTIO_BLK_ID_BYTES as usize
        {
            return Err(ValidationError::InvalidSerialLength(
                serial.len(),
                VIRTIO_BLK_ID_BYTES as usize,
            ));
        }

        Ok(())
    }
}

#[derive(Debug)]
pub enum ParseVhostModeError {
    InvalidValue(String),
}

impl FromStr for VhostMode {
    type Err = ParseVhostModeError;

    fn from_str(s: &str) -> result::Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "client" => Ok(VhostMode::Client),
            "server" => Ok(VhostMode::Server),
            _ => Err(ParseVhostModeError::InvalidValue(s.to_owned())),
        }
    }
}

impl NetConfig {
    pub const SYNTAX: &'static str = "Network parameters \
    \"tap=<if_name>,ip=<ip_addr>,mask=<net_mask>,mac=<mac_addr>,fd=<[fd1,fd2,...]>,iommu=on|off,\
    num_queues=<number_of_queues>,queue_size=<size_of_each_queue>,id=<device_id>,\
    vhost_user=<vhost_user_enable>,socket=<vhost_user_socket_path>,vhost_mode=client|server,\
    bw_size=<bytes>,bw_one_time_burst=<bytes>,bw_refill_time=<ms>,\
    ops_size=<io_ops>,ops_one_time_burst=<io_ops>,ops_refill_time=<ms>,\
    pci_segment=<segment_id>,pci_device_id=<pci_slot>,\
    offload_tso=on|off,offload_ufo=on|off,offload_csum=on|off\"";

    pub fn parse(net: &str) -> Result<Self> {
        let mut parser = OptionParser::new();

        parser
            .add("tap")
            .add("ip")
            .add("mask")
            .add("mac")
            .add("host_mac")
            .add("offload_tso")
            .add("offload_ufo")
            .add("offload_csum")
            .add("mtu")
            .add("queue_size")
            .add("num_queues")
            .add("vhost_user")
            .add("socket")
            .add("vhost_mode")
            .add("fd")
            .add("bw_size")
            .add("bw_one_time_burst")
            .add("bw_refill_time")
            .add("ops_size")
            .add("ops_one_time_burst")
            .add("ops_refill_time")
            .add_all(PciDeviceCommonConfig::OPTIONS_IOMMU);
        parser.parse(net).map_err(Error::ParseNetwork)?;

        let tap = parser.get("tap");
        let ip = parser.convert("ip").map_err(Error::ParseNetwork)?;
        let mask = parser.convert("mask").map_err(Error::ParseNetwork)?;

        let mac = parser
            .convert("mac")
            .map_err(Error::ParseNetwork)?
            .unwrap_or_else(default_netconfig_mac);
        let host_mac = parser.convert("host_mac").map_err(Error::ParseNetwork)?;
        let offload_tso = parser
            .convert::<Toggle>("offload_tso")
            .map_err(Error::ParseNetwork)?
            .unwrap_or(Toggle(true))
            .0;
        let offload_ufo = parser
            .convert::<Toggle>("offload_ufo")
            .map_err(Error::ParseNetwork)?
            .unwrap_or(Toggle(true))
            .0;
        let offload_csum = parser
            .convert::<Toggle>("offload_csum")
            .map_err(Error::ParseNetwork)?
            .unwrap_or(Toggle(true))
            .0;
        let mtu = parser.convert("mtu").map_err(Error::ParseNetwork)?;
        let queue_size = parser
            .convert("queue_size")
            .map_err(Error::ParseNetwork)?
            .unwrap_or_else(default_netconfig_queue_size);
        let num_queues = parser
            .convert("num_queues")
            .map_err(Error::ParseNetwork)?
            .unwrap_or_else(default_netconfig_num_queues);
        let vhost_user = parser
            .convert::<Toggle>("vhost_user")
            .map_err(Error::ParseNetwork)?
            .unwrap_or(Toggle(false))
            .0;
        let vhost_socket = parser.get("socket");
        let vhost_mode = parser
            .convert("vhost_mode")
            .map_err(Error::ParseNetwork)?
            .unwrap_or_default();
        let fds = parser
            .convert::<IntegerList>("fd")
            .map_err(Error::ParseNetwork)?
            .map(|v| v.0.iter().map(|e| *e as i32).collect());
        let bw_size = parser
            .convert("bw_size")
            .map_err(Error::ParseNetwork)?
            .unwrap_or_default();
        let bw_one_time_burst = parser
            .convert("bw_one_time_burst")
            .map_err(Error::ParseNetwork)?
            .unwrap_or_default();
        let bw_refill_time = parser
            .convert("bw_refill_time")
            .map_err(Error::ParseNetwork)?
            .unwrap_or_default();
        let ops_size = parser
            .convert("ops_size")
            .map_err(Error::ParseNetwork)?
            .unwrap_or_default();
        let ops_one_time_burst = parser
            .convert("ops_one_time_burst")
            .map_err(Error::ParseNetwork)?
            .unwrap_or_default();
        let ops_refill_time = parser
            .convert("ops_refill_time")
            .map_err(Error::ParseNetwork)?
            .unwrap_or_default();
        let bw_tb_config = if bw_size != 0 && bw_refill_time != 0 {
            Some(TokenBucketConfig {
                size: bw_size,
                one_time_burst: Some(bw_one_time_burst),
                refill_time: bw_refill_time,
            })
        } else {
            None
        };
        let ops_tb_config = if ops_size != 0 && ops_refill_time != 0 {
            Some(TokenBucketConfig {
                size: ops_size,
                one_time_burst: Some(ops_one_time_burst),
                refill_time: ops_refill_time,
            })
        } else {
            None
        };
        let rate_limiter_config = if bw_tb_config.is_some() || ops_tb_config.is_some() {
            Some(RateLimiterConfig {
                bandwidth: bw_tb_config,
                ops: ops_tb_config,
            })
        } else {
            None
        };

        let pci_common = PciDeviceCommonConfig::parse(net)?;

        let config = NetConfig {
            pci_common,
            tap,
            ip,
            mask,
            mac,
            host_mac,
            mtu,
            num_queues,
            queue_size,
            vhost_user,
            vhost_socket,
            vhost_mode,
            fds,
            rate_limiter_config,
            offload_tso,
            offload_ufo,
            offload_csum,
        };
        Ok(config)
    }

    pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> {
        self.pci_common.validate(vm_config)?;

        if self.num_queues < 2 {
            return Err(ValidationError::VnetQueueLowerThan2(self.num_queues));
        }

        if let Some(fds) = &self.fds {
            let actual_queues = fds.len() * 2;
            if actual_queues != self.num_queues {
                return Err(ValidationError::VnetQueueFdMismatch(
                    self.num_queues,
                    actual_queues,
                ));
            }

            for &fd in fds {
                if fd <= 2 {
                    return Err(ValidationError::VnetReservedFd(fd));
                }
            }
        }

        if (self.num_queues / 2) > vm_config.cpus.boot_vcpus as usize {
            return Err(ValidationError::TooManyQueues(
                self.num_queues,
                vm_config.cpus.boot_vcpus as usize,
            ));
        }

        validate_queue_size(self.queue_size)?;

        if self.vhost_user && self.pci_common.iommu {
            return Err(ValidationError::IommuNotSupported);
        }

        if self.vhost_user && self.rate_limiter_config.is_some() {
            return Err(ValidationError::VhostUserRateLimiterNotSupported);
        }

        if let Some(mtu) = self.mtu
            && mtu < net::MIN_MTU
        {
            return Err(ValidationError::InvalidMtu(mtu));
        }

        if !self.offload_csum && (self.offload_tso || self.offload_ufo) {
            return Err(ValidationError::NoHardwareChecksumOffload);
        }

        if self.mask.is_some() && self.ip.is_none() {
            return Err(ValidationError::MaskProvidedWithoutIp);
        }

        if self.ip.is_some() && self.mask.is_none() {
            return Err(ValidationError::IpProvidedWithoutMask);
        }

        Ok(())
    }
}

impl RngConfig {
    pub const SYNTAX: &'static str = "Random number generator parameters \"\
        src=<entropy_source_path>,iommu=on|off,pci_segment=<segment_id>,\
        pci_device_id=<pci_slot>\"";

    pub fn parse(rng: &str) -> Result<Self> {
        let mut parser = OptionParser::new();
        parser
            .add("src")
            .add_all(PciDeviceCommonConfig::OPTIONS_IOMMU);
        parser.parse(rng).map_err(Error::ParseRng)?;

        let src = PathBuf::from(
            parser
                .get("src")
                .unwrap_or_else(|| Self::DEFAULT_RNG_SOURCE.to_owned()),
        );

        let pci_common = PciDeviceCommonConfig::parse(rng)?;

        Ok(RngConfig { src, pci_common })
    }

    pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> {
        self.pci_common.validate(vm_config)
    }
}

impl RtcConfig {
    pub const SYNTAX: &'static str = "Virtio RTC parameters \"\
        iommu=on|off,id=<device_id>,\
        pci_segment=<segment_id>,pci_device_id=<pci_slot>\". \
        Passing --rtc with no arguments enables the device with default \
        settings.";

    pub fn parse(rtc: &str) -> Result<Self> {
        let mut parser = OptionParser::new();
        parser.add_all(PciDeviceCommonConfig::OPTIONS_IOMMU);
        parser.parse(rtc).map_err(Error::ParseRtc)?;

        let pci_common = PciDeviceCommonConfig::parse(rtc)?;

        Ok(RtcConfig { pci_common })
    }

    pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> {
        self.pci_common.validate(vm_config)
    }
}

impl BalloonConfig {
    pub const SYNTAX: &'static str = "Balloon parameters \"size=<balloon_size>,deflate_on_oom=on|off,\
        free_page_reporting=on|off,iommu=on|off,id=<device_id>,pci_segment=<segment_id>,\
        pci_device_id=<pci_slot>\"";

    pub fn parse(balloon: &str) -> Result<Self> {
        let mut parser = OptionParser::new();
        parser.add("size");
        parser.add("deflate_on_oom");
        parser.add("free_page_reporting");
        parser.add_all(PciDeviceCommonConfig::OPTIONS_IOMMU);
        parser.parse(balloon).map_err(Error::ParseBalloon)?;

        let size = parser
            .convert::<ByteSized>("size")
            .map_err(Error::ParseBalloon)?
            .map_or(0, |v| v.0);

        let deflate_on_oom = parser
            .convert::<Toggle>("deflate_on_oom")
            .map_err(Error::ParseBalloon)?
            .unwrap_or(Toggle(false))
            .0;

        let free_page_reporting = parser
            .convert::<Toggle>("free_page_reporting")
            .map_err(Error::ParseBalloon)?
            .unwrap_or(Toggle(false))
            .0;

        let pci_common = PciDeviceCommonConfig::parse(balloon)?;

        Ok(BalloonConfig {
            pci_common,
            size,
            deflate_on_oom,
            free_page_reporting,
        })
    }

    pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> {
        self.pci_common.validate(vm_config)
    }
}

impl GenericVhostUserConfig {
    pub const SYNTAX: &'static str = "generic vhost-user parameters \
    \"device_type=<ID number for virtio device type (FS, block, net, etc) or symbolic name>,\
    socket=<socket_path>,\
    queue_sizes=<list of queue sizes>,\
    id=<device_id>,pci_segment=<segment_id>,pci_device_id=<pci_slot>\"";

    pub fn parse(vhost_user: &str) -> Result<Self> {
        let mut parser = OptionParser::new();
        parser
            .add("device_type")
            // TODO: Remove 'virtio_id' as a deprecated alias for 'device_type'
            .add("virtio_id")
            .add("queue_sizes")
            .add("socket")
            .add_all(PciDeviceCommonConfig::OPTIONS);
        parser
            .parse(vhost_user)
            .map_err(Error::ParseGenericVhostUser)?;

        let socket = parser
            .get("socket")
            .ok_or(Error::ParseGenericVhostUserSockMissing)?;

        let IntegerList(queue_sizes) = parser
            .convert::<IntegerList<u16>>("queue_sizes")
            .map_err(Error::ParseGenericVhostUser)?
            .ok_or(Error::ParseGenericVhostUserQueueSizeMissing)?;
        let legacy_virtio_id = parser
            .convert::<String>("virtio_id")
            .map_err(Error::ParseGenericVhostUser)?;
        if legacy_virtio_id.is_some() {
            warn!("'virtio_id' in --generic-vhost-user is deprecated; use 'device_type'.");
        }
        let device_type_str = parser
            .convert::<String>("device_type")
            .map_err(Error::ParseGenericVhostUser)?
            .or(legacy_virtio_id)
            .ok_or(Error::ParseGenericVhostUserVirtioIdMissing)?;
        let device_type = match device_type_str.as_bytes() {
            b"net" => VIRTIO_ID_NET,
            b"block" => VIRTIO_ID_BLOCK,
            b"console" => VIRTIO_ID_CONSOLE,
            b"rng" => VIRTIO_ID_RNG,
            b"balloon" => VIRTIO_ID_BALLOON,
            b"iomem" => VIRTIO_ID_IOMEM,
            b"rpmsg" => VIRTIO_ID_RPMSG,
            b"scsi" => VIRTIO_ID_SCSI,
            b"9p" => VIRTIO_ID_9P,
            b"mac80211_wlan" => VIRTIO_ID_MAC80211_WLAN,
            b"rproc_serial" => VIRTIO_ID_RPROC_SERIAL,
            b"caif" => VIRTIO_ID_CAIF,
            b"memory_balloon" => VIRTIO_ID_MEMORY_BALLOON,
            b"gpu" => VIRTIO_ID_GPU,
            b"clock" => VIRTIO_ID_CLOCK,
            b"input" => VIRTIO_ID_INPUT,
            b"vsock" => VIRTIO_ID_VSOCK,
            b"crypto" => VIRTIO_ID_CRYPTO,
            b"signal_dist" => VIRTIO_ID_SIGNAL_DIST,
            b"pstore" => VIRTIO_ID_PSTORE,
            b"iommu" => VIRTIO_ID_IOMMU,
            b"mem" => VIRTIO_ID_MEM,
            b"sound" => VIRTIO_ID_SOUND,
            b"fs" => VIRTIO_ID_FS,
            b"pmem" => VIRTIO_ID_PMEM,
            b"rpmb" => VIRTIO_ID_RPMB,
            b"mac80211_hwsim" => VIRTIO_ID_MAC80211_HWSIM,
            b"video_encoder" => VIRTIO_ID_VIDEO_ENCODER,
            b"video_decoder" => VIRTIO_ID_VIDEO_DECODER,
            b"scmi" => VIRTIO_ID_SCMI,
            b"nitro_sec_mod" => VIRTIO_ID_NITRO_SEC_MOD,
            b"i2c" => VIRTIO_ID_I2C_ADAPTER,
            b"watchdog" => VIRTIO_ID_WATCHDOG,
            b"can" => VIRTIO_ID_CAN,
            b"dmabuf" => VIRTIO_ID_DMABUF,
            b"param_serv" => VIRTIO_ID_PARAM_SERV,
            b"audio_policy" => VIRTIO_ID_AUDIO_POLICY,
            b"bt" => VIRTIO_ID_BT,
            b"gpio" => VIRTIO_ID_GPIO,
            b"rdma" => 42,
            b"camera" => 43,
            b"ism" => 44,
            b"spi" => 45,
            b"tee" => 46,
            b"cpu_balloon" => 47,
            b"media" => 48,
            b"usb" => 49,
            [b'1'..=b'9', ..] => match device_type_str.parse() {
                Ok(id) => id,
                Err(_) => return Err(Error::ParseGenericVhostUserVirtioIdInvalid(device_type_str)),
            },
            _ => return Err(Error::ParseGenericVhostUserVirtioIdInvalid(device_type_str)),
        };
        match device_type {
            // vhost-user devices of these types definitely cannot work.
            // Cloud Hypervisor needs to know if an IOMMU exists so that it
            // can perform address translation, and a vhost-user device has
            // no supported way to reset the guest.
            VIRTIO_ID_WATCHDOG | VIRTIO_ID_IOMMU => {
                return Err(Error::ParseGenericVhostUserVirtioIdUnsupported(
                    device_type_str,
                ));
            }
            _ => {}
        }
        let pci_common = PciDeviceCommonConfig::parse(vhost_user)?;

        Ok(GenericVhostUserConfig {
            pci_common,
            socket: socket.into(),
            device_type,
            queue_sizes,
        })
    }

    pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> {
        if self.pci_common.iommu {
            return Err(ValidationError::IommuNotSupported);
        }

        for &queue_size in &self.queue_sizes {
            validate_queue_size(queue_size)?;
        }

        self.pci_common.validate(vm_config)
    }
}

impl FsConfig {
    pub const SYNTAX: &'static str = "virtio-fs parameters \
    \"tag=<tag_name>,socket=<socket_path>,num_queues=<number_of_queues>,\
    queue_size=<size_of_each_queue>,id=<device_id>,\
    pci_segment=<segment_id>,pci_device_id=<pci_slot>\"";

    pub fn parse(fs: &str) -> Result<Self> {
        let mut parser = OptionParser::new();
        parser
            .add("tag")
            .add("queue_size")
            .add("num_queues")
            .add("socket")
            .add_all(PciDeviceCommonConfig::OPTIONS);
        parser.parse(fs).map_err(Error::ParseFileSystem)?;

        let tag = parser.get("tag").ok_or(Error::ParseFsTagMissing)?;
        if tag.len() > vhost_user::VIRTIO_FS_TAG_LEN {
            return Err(Error::ParseFsTagTooLong);
        }
        let socket = PathBuf::from(parser.get("socket").ok_or(Error::ParseFsSockMissing)?);

        let queue_size = parser
            .convert("queue_size")
            .map_err(Error::ParseFileSystem)?
            .unwrap_or_else(default_fsconfig_queue_size);
        let num_queues = parser
            .convert("num_queues")
            .map_err(Error::ParseFileSystem)?
            .unwrap_or_else(default_fsconfig_num_queues);

        let pci_common = PciDeviceCommonConfig::parse(fs)?;

        Ok(FsConfig {
            pci_common,
            tag,
            socket,
            num_queues,
            queue_size,
        })
    }

    pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> {
        if self.num_queues > vm_config.cpus.boot_vcpus as usize {
            return Err(ValidationError::TooManyQueues(
                self.num_queues,
                vm_config.cpus.boot_vcpus as usize,
            ));
        }

        validate_queue_size(self.queue_size)?;

        if self.pci_common.iommu {
            return Err(ValidationError::IommuNotSupported);
        }

        self.pci_common.validate(vm_config)
    }
}

#[cfg(feature = "fw_cfg")]
impl FwCfgConfig {
    pub const SYNTAX: &'static str = "Boot params to pass to FW CFG device \
    \"e820=on|off,kernel=on|off,cmdline=on|off,initramfs=on|off,acpi_table=on|off, \
    items=[name=<item_name>,file=<file_path>:name=<item_name>,string=<string_value>]\"";
    pub fn parse(fw_cfg_config: &str) -> Result<Self> {
        let mut parser = OptionParser::new();
        parser
            .add("e820")
            .add("kernel")
            .add("cmdline")
            .add("initramfs")
            .add("acpi_table")
            .add("items");
        parser.parse(fw_cfg_config).map_err(Error::ParseFwCfgItem)?;
        let e820 = parser
            .convert::<Toggle>("e820")
            .map_err(Error::ParseFwCfgItem)?
            .unwrap_or(Toggle(true))
            .0;
        let kernel = parser
            .convert::<Toggle>("kernel")
            .map_err(Error::ParseFwCfgItem)?
            .unwrap_or(Toggle(true))
            .0;
        let cmdline = parser
            .convert::<Toggle>("cmdline")
            .map_err(Error::ParseFwCfgItem)?
            .unwrap_or(Toggle(true))
            .0;
        let initramfs = parser
            .convert::<Toggle>("initramfs")
            .map_err(Error::ParseFwCfgItem)?
            .unwrap_or(Toggle(true))
            .0;
        let acpi_tables = parser
            .convert::<Toggle>("acpi_table")
            .map_err(Error::ParseFwCfgItem)?
            .unwrap_or(Toggle(true))
            .0;
        let items = if parser.is_set("items") {
            Some(
                parser
                    .convert::<FwCfgItemList>("items")
                    .map_err(Error::ParseFwCfgItem)?
                    .unwrap(),
            )
        } else {
            None
        };

        Ok(FwCfgConfig {
            e820,
            kernel,
            cmdline,
            initramfs,
            acpi_tables,
            items,
        })
    }
    pub fn validate(&self, payload: &PayloadConfig) -> result::Result<(), PayloadConfigError> {
        if self.kernel && payload.kernel.is_none() {
            return Err(PayloadConfigError::FwCfgMissingKernel);
        } else if self.cmdline && payload.cmdline.is_none() {
            return Err(PayloadConfigError::FwCfgMissingCmdline);
        } else if self.initramfs && payload.initramfs.is_none() {
            return Err(PayloadConfigError::FwCfgMissingInitramfs);
        }

        if let Some(items) = &self.items {
            for item in &items.item_list {
                if item.file.is_some() == item.string.is_some() {
                    return Err(PayloadConfigError::FwCfgInvalidItem(item.name.clone()));
                }
            }
        }

        Ok(())
    }
}

#[cfg(feature = "fw_cfg")]
impl FwCfgItem {
    pub fn parse(fw_cfg: &str) -> Result<Self> {
        let mut parser = OptionParser::new();
        parser.add("name").add("file").add("string");
        parser.parse(fw_cfg).map_err(Error::ParseFwCfgItem)?;

        let name =
            parser
                .get("name")
                .ok_or(Error::ParseFwCfgItem(OptionParserError::InvalidValue(
                    "missing FwCfgItem name".to_string(),
                )))?;
        let file = parser.get("file").map(PathBuf::from);
        let string = parser.get("string");
        Ok(FwCfgItem { name, file, string })
    }
}

impl PmemConfig {
    pub const SYNTAX: &'static str = "Persistent memory parameters \
    \"file=<backing_file_path>,size=<persistent_memory_size>,iommu=on|off,\
    discard_writes=on|off,id=<device_id>,\
    pci_segment=<segment_id>,pci_device_id=<pci_slot>\"";

    pub fn parse(pmem: &str) -> Result<Self> {
        let mut parser = OptionParser::new();
        parser
            .add("size")
            .add("file")
            .add("discard_writes")
            .add_all(PciDeviceCommonConfig::OPTIONS_IOMMU);
        parser.parse(pmem).map_err(Error::ParsePersistentMemory)?;

        let pci_common = PciDeviceCommonConfig::parse(pmem)?;
        let file = PathBuf::from(parser.get("file").ok_or(Error::ParsePmemFileMissing)?);
        let size = parser
            .convert::<ByteSized>("size")
            .map_err(Error::ParsePersistentMemory)?
            .map(|v| v.0);
        let discard_writes = parser
            .convert::<Toggle>("discard_writes")
            .map_err(Error::ParsePersistentMemory)?
            .unwrap_or(Toggle(false))
            .0;

        Ok(PmemConfig {
            pci_common,
            file,
            size,
            discard_writes,
        })
    }

    pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> {
        self.pci_common.validate(vm_config)
    }
}

impl CommonConsoleConfig {
    const VALUELESS_OPTIONS: &[&str] = &["off", "pty", "tty", "null"];
    const VALUE_OPTIONS: &[&str] = &["file", "socket"];

    fn parse(console: &str, map_err: impl Fn(OptionParserError) -> Error) -> Result<Self> {
        let mut parser = OptionParser::new();
        parser
            .add_all_valueless(Self::VALUELESS_OPTIONS)
            .add_all(Self::VALUE_OPTIONS);
        parser.parse_subset(console).map_err(map_err)?;

        let mut file: Option<PathBuf> = None;
        let mut socket: Option<PathBuf> = None;
        let mut mode: ConsoleOutputMode = ConsoleOutputMode::Off;

        if parser.is_set("off") {
        } else if parser.is_set("pty") {
            mode = ConsoleOutputMode::Pty;
        } else if parser.is_set("tty") {
            mode = ConsoleOutputMode::Tty;
        } else if parser.is_set("null") {
            mode = ConsoleOutputMode::Null;
        } else if parser.is_set("file") {
            mode = ConsoleOutputMode::File;
            file =
                Some(PathBuf::from(parser.get("file").ok_or(
                    Error::Validation(ValidationError::ConsoleFileMissing),
                )?));
        } else if parser.is_set("socket") {
            mode = ConsoleOutputMode::Socket;
            socket = Some(PathBuf::from(parser.get("socket").ok_or(
                Error::Validation(ValidationError::ConsoleSocketPathMissing),
            )?));
        } else {
            return Err(Error::ParseConsoleInvalidModeGiven);
        }

        Ok(Self { mode, file, socket })
    }
}

impl ConsoleConfig {
    pub fn parse(console: &str) -> Result<Self> {
        let mut parser = OptionParser::new();
        parser
            .add_all_valueless(CommonConsoleConfig::VALUELESS_OPTIONS)
            .add_all(CommonConsoleConfig::VALUE_OPTIONS)
            .add_all(PciDeviceCommonConfig::OPTIONS_IOMMU);
        parser.parse(console).map_err(Error::ParseConsole)?;

        let common = CommonConsoleConfig::parse(console, Error::ParseConsole)?;
        let pci_common = PciDeviceCommonConfig::parse(console)?;

        Ok(Self { common, pci_common })
    }

    pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> {
        self.pci_common.validate(vm_config)
    }
}

impl SerialConfig {
    pub fn parse(serial: &str) -> Result<Self> {
        let mut parser = OptionParser::new();
        parser
            .add_all_valueless(CommonConsoleConfig::VALUELESS_OPTIONS)
            .add_all(CommonConsoleConfig::VALUE_OPTIONS);
        parser.parse(serial).map_err(Error::ParseSerial)?;

        let common = CommonConsoleConfig::parse(serial, Error::ParseSerial)?;
        Ok(Self { common })
    }
}

#[cfg(target_arch = "x86_64")]
impl DebugConsoleConfig {
    pub fn parse(debug_console_ops: &str) -> Result<Self> {
        let mut parser = OptionParser::new();
        parser
            .add_valueless("off")
            .add_valueless("pty")
            .add_valueless("tty")
            .add_valueless("null")
            .add("file")
            .add("iobase");
        parser
            .parse(debug_console_ops)
            .map_err(Error::ParseConsole)?;

        let mut file: Option<PathBuf> = None;
        let mut iobase: Option<u16> = None;
        let mut mode: ConsoleOutputMode = ConsoleOutputMode::Off;

        if parser.is_set("off") {
        } else if parser.is_set("pty") {
            mode = ConsoleOutputMode::Pty;
        } else if parser.is_set("tty") {
            mode = ConsoleOutputMode::Tty;
        } else if parser.is_set("null") {
            mode = ConsoleOutputMode::Null;
        } else if parser.is_set("file") {
            mode = ConsoleOutputMode::File;
            file =
                Some(PathBuf::from(parser.get("file").ok_or(
                    Error::Validation(ValidationError::ConsoleFileMissing),
                )?));
        } else {
            return Err(Error::ParseConsoleInvalidModeGiven);
        }

        if parser.is_set("iobase")
            && let Some(iobase_opt) = parser.get("iobase")
        {
            if !iobase_opt.starts_with("0x") {
                return Err(Error::Validation(ValidationError::InvalidIoPortHex(
                    iobase_opt,
                )));
            }
            iobase =
                Some(u16::from_str_radix(&iobase_opt[2..], 16).map_err(|_| {
                    Error::Validation(ValidationError::InvalidIoPortHex(iobase_opt))
                })?);
        }

        Ok(Self { file, mode, iobase })
    }
}

impl DeviceConfig {
    pub const SYNTAX: &'static str = "Direct device assignment parameters \
    \"path=<device_path>,fd=<vfio_cdev_fd>,iommu=on|off,id=<device_id>,\
    pci_segment=<segment_id>,pci_device_id=<pci_slot>,\
    x_nv_gpudirect_clique=<clique_id>,\
    x_exclude_mmap_bars=[<bar>...]\"";

    pub fn parse(device: &str) -> Result<Self> {
        let mut parser = OptionParser::new();
        parser
            .add("path")
            .add("fd")
            .add_all(PciDeviceCommonConfig::OPTIONS_IOMMU)
            .add("x_nv_gpudirect_clique")
            .add("x_exclude_mmap_bars");
        parser.parse(device).map_err(Error::ParseDevice)?;

        let pci_common = PciDeviceCommonConfig::parse(device)?;
        let path = parser.get("path").map(PathBuf::from);
        let fd = parser.convert::<i32>("fd").map_err(Error::ParseDevice)?;
        let x_nv_gpudirect_clique = parser
            .convert::<u8>("x_nv_gpudirect_clique")
            .map_err(Error::ParseDevice)?;
        let x_exclude_mmap_bars = parser
            .convert::<IntegerList>("x_exclude_mmap_bars")
            .map_err(Error::ParseDevice)?
            .map(|bars| bars.0)
            .unwrap_or_default();
        Ok(DeviceConfig {
            pci_common,
            path,
            fd,
            x_nv_gpudirect_clique,
            x_exclude_mmap_bars,
        })
    }

    pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> {
        self.pci_common.validate(vm_config)?;

        match (&self.path, self.fd) {
            (None, None) => return Err(ValidationError::VfioDeviceNeitherPathNorFd),
            (Some(_), Some(_)) => return Err(ValidationError::VfioDeviceBothPathAndFd),
            (None, Some(_)) => {
                let iommufd_fd_set = vm_config
                    .platform
                    .as_ref()
                    .is_some_and(|p| p.iommufd_fd.is_some());
                if !iommufd_fd_set {
                    return Err(ValidationError::VfioFdRequiresIommufdFd);
                }
            }
            (Some(_), None) => {}
        }

        if self.x_nv_gpudirect_clique.is_some() {
            let vfio_p2p_dma = vm_config.platform.as_ref().is_none_or(|p| p.vfio_p2p_dma);
            if !vfio_p2p_dma {
                return Err(ValidationError::GpuDirectCliqueRequiresP2pDma);
            }
        }

        // PCI devices expose six BARs, so only BAR indices 0 through 5 are valid here.
        for bar in &self.x_exclude_mmap_bars {
            if *bar > 5 {
                return Err(ValidationError::InvalidDeviceExcludeMmapBar(*bar));
            }
        }

        Ok(())
    }
}

impl UserDeviceConfig {
    pub const SYNTAX: &'static str = "Userspace device socket=<socket_path>,id=<device_id>,\
        pci_segment=<segment_id>,pci_device_id=<pci_slot>\"";

    pub fn parse(user_device: &str) -> Result<Self> {
        let mut parser = OptionParser::new();
        parser.add("socket").add_all(PciDeviceCommonConfig::OPTIONS);
        parser.parse(user_device).map_err(Error::ParseUserDevice)?;

        let pci_common = PciDeviceCommonConfig::parse(user_device)?;
        let socket = parser
            .get("socket")
            .map(PathBuf::from)
            .ok_or(Error::ParseUserDeviceSocketMissing)?;

        Ok(UserDeviceConfig { pci_common, socket })
    }

    pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> {
        if self.pci_common.iommu {
            return Err(ValidationError::IommuNotSupported);
        }

        self.pci_common.validate(vm_config)
    }
}

impl VdpaConfig {
    pub const SYNTAX: &'static str = "vDPA device \
        \"path=<device_path>,num_queues=<number_of_queues>,iommu=on|off,\
        id=<device_id>,pci_segment=<segment_id>,pci_device_id=<pci_slot>\"";

    pub fn parse(vdpa: &str) -> Result<Self> {
        let mut parser = OptionParser::new();
        parser
            .add("path")
            .add("num_queues")
            .add_all(PciDeviceCommonConfig::OPTIONS_IOMMU);
        parser.parse(vdpa).map_err(Error::ParseVdpa)?;

        let pci_common = PciDeviceCommonConfig::parse(vdpa)?;
        let path = parser
            .get("path")
            .map(PathBuf::from)
            .ok_or(Error::ParseVdpaPathMissing)?;
        let num_queues = parser
            .convert("num_queues")
            .map_err(Error::ParseVdpa)?
            .unwrap_or_else(default_vdpaconfig_num_queues);

        Ok(VdpaConfig {
            pci_common,
            path,
            num_queues,
        })
    }

    pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> {
        self.pci_common.validate(vm_config)
    }
}

impl VsockConfig {
    pub const SYNTAX: &'static str = "Virtio VSOCK parameters \
        \"cid=<context_id>,socket=<socket_path>,iommu=on|off,id=<device_id>,\
        pci_segment=<segment_id>,pci_device_id=<pci_slot>\"";

    pub fn parse(vsock: &str) -> Result<Self> {
        let mut parser = OptionParser::new();
        parser
            .add("socket")
            .add("cid")
            .add_all(PciDeviceCommonConfig::OPTIONS_IOMMU);
        parser.parse(vsock).map_err(Error::ParseVsock)?;

        let pci_common = PciDeviceCommonConfig::parse(vsock)?;
        let socket = parser
            .get("socket")
            .map(PathBuf::from)
            .ok_or(Error::ParseVsockSockMissing)?;
        let cid = parser
            .convert("cid")
            .map_err(Error::ParseVsock)?
            .ok_or(Error::ParseVsockCidMissing)?;

        Ok(VsockConfig {
            pci_common,
            cid,
            socket,
        })
    }

    pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> {
        self.pci_common.validate(vm_config)
    }
}

impl NumaConfig {
    pub const SYNTAX: &'static str = "Settings related to a given NUMA node \
        \"guest_numa_id=<node_id>,cpus=<cpus_id>,distances=<list_of_distances_to_destination_nodes>,\
        device_id=<device_id>,\
        memory_zones=<list_of_memory_zones>,\
        pci_segments=<list_of_pci_segments>\"";

    pub fn parse(numa: &str) -> Result<Self> {
        let mut parser = OptionParser::new();
        parser
            .add("guest_numa_id")
            .add("cpus")
            .add("distances")
            .add("device_id")
            .add("memory_zones")
            .add("pci_segments");

        parser.parse(numa).map_err(Error::ParseNuma)?;

        let guest_numa_id = parser
            .convert::<u32>("guest_numa_id")
            .map_err(Error::ParseNuma)?
            .ok_or_else(|| {
                Error::ParseNuma(OptionParserError::InvalidValue(
                    "guest_numa_id is required for all NUMA nodes".to_string(),
                ))
            })?;
        let cpus = parser
            .convert::<IntegerList>("cpus")
            .map_err(Error::ParseNuma)?
            .map(|v| v.0.iter().map(|e| *e as u32).collect());
        let distances = parser
            .convert::<Tuple<u64, u64>>("distances")
            .map_err(Error::ParseNuma)?
            .map(|v| {
                v.0.iter()
                    .map(|(e1, e2)| NumaDistance {
                        destination: *e1 as u32,
                        distance: *e2 as u8,
                    })
                    .collect()
            });
        let device_id = parser.get("device_id");
        let memory_zones = parser
            .convert::<StringList>("memory_zones")
            .map_err(Error::ParseNuma)?
            .map(|v| v.0.into_boxed_slice());
        let pci_segments = parser
            .convert::<IntegerList>("pci_segments")
            .map_err(Error::ParseNuma)?
            .map(|v| v.0.iter().map(|e| *e as u16).collect());
        if device_id.is_some() && (cpus.is_some() || memory_zones.is_some()) {
            return Err(Error::ParseNuma(OptionParserError::InvalidValue(
                "device_id in numa config cannot be used with cpus or memory zones".to_string(),
            )));
        }
        Ok(NumaConfig {
            guest_numa_id,
            cpus,
            distances,
            device_id,
            memory_zones,
            pci_segments,
        })
    }

    pub fn is_generic_initiator(&self) -> bool {
        self.device_id.is_some()
    }

    /// Validates NumaConfig
    pub fn validate(&self) -> result::Result<(), ValidationError> {
        match (&self.device_id, &self.cpus, &self.memory_zones) {
            (Some(device_id), None, None) => {
                // Valid generic initiator case
                if device_id.is_empty() {
                    return Err(ValidationError::InvalidNumaConfig(
                        "device_id in numa config cannot be empty".to_string(),
                    ));
                }
                Ok(())
            }
            (None, Some(cpus), _) => {
                // Standard NUMA with cpus
                if cpus.is_empty() {
                    return Err(ValidationError::InvalidNumaConfig(
                        "cpus list in numa config cannot be empty".to_string(),
                    ));
                }
                Ok(())
            }
            (None, _, Some(memory_zones)) => {
                // Standard NUMA with memory_zones (cpus is None here)
                if memory_zones.is_empty() {
                    return Err(ValidationError::InvalidNumaConfig(
                        "memory_zones in numa config cannot be empty".to_string(),
                    ));
                }
                Ok(())
            }
            _ => {
                // Default handles all error cases
                if self.device_id.is_some() && (self.cpus.is_some() || self.memory_zones.is_some())
                {
                    Err(ValidationError::InvalidNumaConfig(
                        "device_id in numa config is mutually exclusive with cpus and memory_zones"
                            .to_string(),
                    ))
                } else {
                    Err(ValidationError::InvalidNumaConfig(
                        "numa config must specify either device_id or cpus/memory_zones"
                            .to_string(),
                    ))
                }
            }
        }
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)]
pub struct RestoredNetConfig {
    pub id: String,
    #[serde(default)]
    pub num_fds: usize,
    // Special deserialize handling:
    // A serialize-deserialize cycle typically happens across processes.
    // Therefore, we don't serialize FDs, and whatever value is here after
    // deserialization is invalid.
    //
    // Valid FDs are transmitted via a different channel (SCM_RIGHTS message)
    // and will be populated into this struct on the destination VMM eventually.
    #[serde(default, deserialize_with = "deserialize_restorednetconfig_fds")]
    pub fds: Option<Vec<i32>>,
}

fn deserialize_restorednetconfig_fds<'de, D>(d: D) -> result::Result<Option<Vec<i32>>, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let invalid_fds: Option<Vec<i32>> = Option::deserialize(d)?;
    if let Some(invalid_fds) = invalid_fds {
        // If the live-migration path is used properly, new FDs are passed as
        // SCM_RIGHTS message. So, we don't get them from the serialized JSON
        // anyway.
        debug!(
            "FDs in 'RestoredNetConfig' won't be deserialized as they are most likely invalid now. Deserializing them as -1."
        );
        Ok(Some(vec![-1; invalid_fds.len()]))
    } else {
        Ok(None)
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize, Default)]
pub enum MemoryRestoreMode {
    /// Restore by eagerly copying the snapshot into guest RAM before resume.
    #[default]
    Copy,
    /// Restore lazily by faulting snapshot pages into guest RAM on demand.
    OnDemand,
}

#[derive(Debug, Error)]
pub enum MemoryRestoreModeParseError {
    #[error("Invalid value: {0}")]
    InvalidValue(String),
}

impl FromStr for MemoryRestoreMode {
    type Err = MemoryRestoreModeParseError;

    fn from_str(s: &str) -> result::Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "copy" => Ok(Self::Copy),
            "ondemand" => Ok(Self::OnDemand),
            _ => Err(MemoryRestoreModeParseError::InvalidValue(s.to_owned())),
        }
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)]
pub struct RestoreConfig {
    pub source_url: PathBuf,
    #[serde(default)]
    pub prefault: bool,
    #[serde(default)]
    pub memory_restore_mode: MemoryRestoreMode,
    #[serde(default)]
    pub net_fds: Option<Vec<RestoredNetConfig>>,
    #[serde(default)]
    pub resume: bool,
}

impl RestoreConfig {
    pub const SYNTAX: &'static str = "Restore from a VM snapshot. \
        \nRestore parameters \"source_url=<source_url>,prefault=on|off,memory_restore_mode=copy|ondemand,\
        net_fds=<list_of_net_ids_with_their_associated_fds>,resume=true|false\" \
        \n`source_url` should be a valid URL (e.g file:///foo/bar or tcp://192.168.1.10/foo) \
        \n`prefault` controls eager prefaulting for the copy-based restore path (disabled by default) \
        \n`memory_restore_mode=copy` preserves the existing eager read-copy restore behavior, while `memory_restore_mode=ondemand` enables lazy demand paging and fails restore if userfaultfd support is unavailable \
        \n`net_fds` is a list of net ids with new file descriptors. \
        Only net devices backed by FDs directly are needed as input.\
        \n `resume` controls whether the VM will be directly resumed after restore ";

    pub fn parse(restore: &str) -> Result<Self> {
        let mut parser = OptionParser::new();
        parser
            .add("source_url")
            .add("prefault")
            .add("memory_restore_mode")
            .add("net_fds")
            .add("resume");
        parser.parse(restore).map_err(Error::ParseRestore)?;

        let source_url = parser
            .get("source_url")
            .map(PathBuf::from)
            .ok_or(Error::ParseRestoreSourceUrlMissing)?;
        let prefault = parser
            .convert::<Toggle>("prefault")
            .map_err(Error::ParseRestore)?
            .unwrap_or(Toggle(false))
            .0;
        let memory_restore_mode = parser
            .convert::<MemoryRestoreMode>("memory_restore_mode")
            .map_err(Error::ParseRestore)?
            .unwrap_or_default();
        let net_fds = parser
            .convert::<Tuple<String, Vec<u64>>>("net_fds")
            .map_err(Error::ParseRestore)?
            .map(|v| {
                v.0.iter()
                    .map(|(id, fds)| RestoredNetConfig {
                        id: id.clone(),
                        num_fds: fds.len(),
                        fds: Some(fds.iter().map(|e| *e as i32).collect()),
                    })
                    .collect()
            });
        let resume = parser
            .convert::<Toggle>("resume")
            .map_err(Error::ParseRestore)?
            .unwrap_or(Toggle(false))
            .0;

        Ok(RestoreConfig {
            source_url,
            prefault,
            memory_restore_mode,
            net_fds,
            resume,
        })
    }

    // Ensure all net devices from 'VmConfig' backed by FDs have a
    // corresponding 'RestoreNetConfig' with a matched 'id' and expected
    // number of FDs.
    pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> {
        if self.memory_restore_mode == MemoryRestoreMode::OnDemand && self.prefault {
            return Err(ValidationError::InvalidRestorePrefaultWithOnDemand);
        }

        let mut restored_net_with_fds = HashMap::new();
        for n in self.net_fds.iter().flatten() {
            assert_eq!(
                n.num_fds,
                n.fds.as_ref().map_or(0, |f| f.len()),
                "Invalid 'RestoredNetConfig' with conflicted fields."
            );
            if restored_net_with_fds.insert(n.id.clone(), n).is_some() {
                return Err(ValidationError::IdentifierNotUnique(n.id.clone()));
            }
        }

        for net_fds in vm_config.net.iter().flatten() {
            if let Some(expected_fds) = &net_fds.fds {
                let expected_id = net_fds
                    .pci_common
                    .id
                    .as_ref()
                    .expect("Invalid 'NetConfig' with empty 'id' for VM restore.");
                if let Some(r) = restored_net_with_fds.remove(expected_id) {
                    if r.num_fds != expected_fds.len() {
                        return Err(ValidationError::RestoreNetFdCountMismatch(
                            expected_id.clone(),
                            r.num_fds,
                            expected_fds.len(),
                        ));
                    }
                } else {
                    return Err(ValidationError::RestoreMissingRequiredNetId(
                        expected_id.clone(),
                    ));
                }
            }
        }

        if !restored_net_with_fds.is_empty() {
            warn!("Ignoring unused 'net_fds' for VM restore.");
        }

        Ok(())
    }
}

impl TpmConfig {
    pub const SYNTAX: &'static str = "TPM device \
        \"(UNIX Domain Socket from swtpm) socket=</path/to/a/socket>\"";

    pub fn parse(tpm: &str) -> Result<Self> {
        let mut parser = OptionParser::new();
        parser.add("socket");
        parser.parse(tpm).map_err(Error::ParseTpm)?;
        let socket = parser
            .get("socket")
            .map(PathBuf::from)
            .ok_or(Error::ParseTpmPathMissing)?;
        Ok(TpmConfig { socket })
    }
}

impl LandlockConfig {
    pub const SYNTAX: &'static str = "Landlock parameters \
        \"path=<path/to/{file/dir}>,access=[rw]\"";

    pub fn parse(landlock_rule: &str) -> Result<Self> {
        let mut parser = OptionParser::new();
        parser.add("path").add("access");
        parser
            .parse(landlock_rule)
            .map_err(Error::ParseLandlockRules)?;

        let path = parser
            .get("path")
            .map(PathBuf::from)
            .ok_or(Error::ParseLandlockMissingFields)?;

        let access = parser
            .get("access")
            .ok_or(Error::ParseLandlockMissingFields)?;

        if access.chars().count() > 2 {
            return Err(Error::ParseLandlockRules(OptionParserError::InvalidValue(
                access.to_string(),
            )));
        }

        Ok(LandlockConfig { path, access })
    }

    pub fn validate(&self) -> ValidationResult<()> {
        if !self.path.exists() {
            return Err(ValidationError::LandlockPathDoesNotExist(self.path.clone()));
        }
        LandlockAccess::try_from(self.access.as_str())
            .map_err(|e| ValidationError::InvalidLandlockAccess(e.to_string()))?;
        Ok(())
    }
}

#[cfg(feature = "ivshmem")]
impl IvshmemConfig {
    pub const SYNTAX: &'static str = "Ivshmem device. Specify the backend file path and size \
    for the shared memory: \"path=</path/to/a/file>,size=<file_size>,id=<device_id>,\
    pci_segment=<segment_id>,pci_device_id=<pci_slot>\" \
    \nThe <file_size> must be a power of 2 (e.g., 2M, 4M, etc.), as it represents the size \
    of the memory region mapped to the guest. Default size is 128M.";
    pub fn parse(ivshmem: &str) -> Result<Self> {
        let mut parser = OptionParser::new();
        parser.add("path").add("size");
        parser.add_all(PciDeviceCommonConfig::OPTIONS);
        parser.parse(ivshmem).map_err(Error::ParseIvshmem)?;
        let path = parser
            .get("path")
            .map(PathBuf::from)
            .ok_or(Error::ParseIvshmemPathMissing)?;
        let size = parser
            .convert::<ByteSized>("size")
            .map_err(Error::ParseIvshmem)?
            .unwrap_or(ByteSized((DEFAULT_IVSHMEM_SIZE << 20) as u64))
            .0;
        let pci_common = PciDeviceCommonConfig::parse(ivshmem)?;
        Ok(IvshmemConfig {
            pci_common,
            path,
            size: size as usize,
        })
    }

    pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> {
        if self.pci_common.iommu {
            return Err(ValidationError::IommuNotSupported);
        }
        self.pci_common.validate(vm_config)?;

        let size = self.size as u64;
        let path = &self.path;
        // size must = 2^n
        if !size.is_power_of_two() {
            return Err(ValidationError::InvalidIvshmemInputSize(size));
        }
        let metadata = fs::metadata(path.to_str().unwrap())
            .map_err(|_| ValidationError::InvalidIvshmemPath)?;
        if metadata.len() < size {
            return Err(ValidationError::InvalidIvshmemSize(metadata.len()));
        }
        Ok(())
    }
}

impl VmConfig {
    fn validate_identifier(
        id_list: &mut BTreeSet<String>,
        id: &Option<String>,
    ) -> ValidationResult<()> {
        if let Some(id) = id.as_ref() {
            if id.starts_with("__") {
                return Err(ValidationError::InvalidIdentifier(id.clone()));
            }

            if !id_list.insert(id.clone()) {
                return Err(ValidationError::IdentifierNotUnique(id.clone()));
            }
        }

        Ok(())
    }

    pub fn backed_by_shared_memory(&self) -> bool {
        if self.memory.shared || self.memory.hugepages {
            return true;
        }

        if self.memory.size == 0 {
            for zone in self.memory.zones.as_ref().unwrap() {
                if !zone.shared && !zone.hugepages {
                    return false;
                }
            }
            true
        } else {
            false
        }
    }

    // Also enables virtio-iommu if the config needs it
    // Returns the list of unique identifiers provided through the
    // configuration.
    pub fn validate(&mut self) -> ValidationResult<BTreeSet<String>> {
        let mut id_list = BTreeSet::new();

        // Is the payload configuration bootable?
        self.payload
            .as_mut()
            .ok_or(ValidationError::PayloadError(
                PayloadConfigError::MissingBootitem,
            ))?
            .validate()?;

        #[cfg(feature = "tdx")]
        {
            let tdx_enabled = self.platform.as_ref().is_some_and(|p| p.tdx);
            // At this point we know payload isn't None.
            if tdx_enabled && self.payload.as_ref().unwrap().firmware.is_none() {
                return Err(ValidationError::TdxFirmwareMissing);
            }
            if tdx_enabled && (self.cpus.max_vcpus != self.cpus.boot_vcpus) {
                return Err(ValidationError::TdxNoCpuHotplug);
            }
        }

        #[cfg(feature = "sev_snp")]
        {
            let sev_snp_enabled = self.platform.as_ref().is_some_and(|p| p.sev_snp);
            if sev_snp_enabled {
                let host_data_opt = &self.payload.as_ref().unwrap().host_data;
                if let Some(host_data) = host_data_opt
                    && host_data.len() != 64
                {
                    return Err(ValidationError::InvalidHostData);
                }
                // KVM SEV-SNP requires an IGVM payload to initialise the VMSA.
                // Without IGVM the vCPU register state is undefined and VM entry fails.
                #[cfg(feature = "igvm")]
                if self
                    .payload
                    .as_ref()
                    .and_then(|p| p.igvm.as_ref())
                    .is_none()
                {
                    return Err(ValidationError::SevSnpRequiresIgvm);
                }
            }
        }
        // The 'conflict' check is introduced in commit 24438e0390d3
        // (vm-virtio: Enable the vmm support for virtio-console).
        //
        // Allow simultaneously set serial and console as TTY mode, for
        // someone want to use virtio console for better performance, and
        // want to keep legacy serial to catch boot stage logs for debug.
        // Using such double tty mode, you need to configure the kernel
        // properly, such as:
        // "console=hvc0 earlyprintk=ttyS0"

        let mut tty_consoles = Vec::new();
        if self.console.common.mode == ConsoleOutputMode::Tty {
            tty_consoles.push("virtio-console");
        }
        if self.serial.common.mode == ConsoleOutputMode::Tty {
            tty_consoles.push("serial-console");
        }
        #[cfg(target_arch = "x86_64")]
        if self.debug_console.mode == ConsoleOutputMode::Tty {
            tty_consoles.push("debug-console");
        }
        if tty_consoles.len() > 1 {
            warn!("Using TTY output for multiple consoles: {tty_consoles:?}");
        }

        if self.console.common.mode == ConsoleOutputMode::File && self.console.common.file.is_none()
        {
            return Err(ValidationError::ConsoleFileMissing);
        }

        if self.serial.common.mode == ConsoleOutputMode::File && self.serial.common.file.is_none() {
            return Err(ValidationError::ConsoleFileMissing);
        }

        if self.cpus.max_vcpus < self.cpus.boot_vcpus {
            return Err(ValidationError::CpusMaxLowerThanBoot(
                self.cpus.max_vcpus,
                self.cpus.boot_vcpus,
            ));
        }

        if self.cpus.max_vcpus > MAX_SUPPORTED_CPUS {
            // Note: historically, Cloud Hypervisor did not support more than 255(254 on x64)
            // vCPUs: self.cpus.max_vcpus was of type u8, so 255 was the maximum;
            // on x86_64, the legacy mptable/apic was limited to 254 CPUs.
            //
            // Now the limit is lifted on x86_64 targets. Other targests/archs: TBD.
            return Err(ValidationError::TooManyCpus(self.cpus.max_vcpus));
        }

        if let Some(rate_limit_groups) = &self.rate_limit_groups {
            for rate_limit_group in rate_limit_groups {
                rate_limit_group.validate(self)?;

                Self::validate_identifier(&mut id_list, &Some(rate_limit_group.id.clone()))?;
            }
        }

        if let Some(disks) = &self.disks {
            for disk in disks {
                if disk.vhost_socket.as_ref().and(disk.path.as_ref()).is_some() {
                    return Err(ValidationError::DiskSocketAndPath);
                }
                if disk.vhost_user && !self.backed_by_shared_memory() {
                    return Err(ValidationError::VhostUserRequiresSharedMemory);
                }
                if disk.vhost_user && disk.vhost_socket.is_none() {
                    return Err(ValidationError::VhostUserMissingSocket);
                }
                if disk.vhost_user
                    && (disk.rate_limiter_config.is_some() || disk.rate_limit_group.is_some())
                {
                    return Err(ValidationError::VhostUserRateLimiterNotSupported);
                }
                if let Some(rate_limit_group) = &disk.rate_limit_group {
                    if let Some(rate_limit_groups) = &self.rate_limit_groups {
                        if !rate_limit_groups
                            .iter()
                            .any(|cfg| &cfg.id == rate_limit_group)
                        {
                            return Err(ValidationError::InvalidRateLimiterGroup);
                        }
                    } else {
                        return Err(ValidationError::InvalidRateLimiterGroup);
                    }
                }

                disk.validate(self)?;
                self.iommu |= disk.pci_common.iommu;

                Self::validate_identifier(&mut id_list, &disk.pci_common.id)?;
            }
        }

        if let Some(nets) = &self.net {
            for net in nets {
                if net.vhost_user && !self.backed_by_shared_memory() {
                    return Err(ValidationError::VhostUserRequiresSharedMemory);
                }
                if net.vhost_user && net.vhost_socket.is_none() {
                    return Err(ValidationError::VhostUserMissingSocket);
                }
                if net.vhost_user && net.rate_limiter_config.is_some() {
                    return Err(ValidationError::VhostUserRateLimiterNotSupported);
                }
                net.validate(self)?;
                self.iommu |= net.pci_common.iommu;

                Self::validate_identifier(&mut id_list, &net.pci_common.id)?;
            }
        }

        if let Some(fses) = &self.fs {
            if !fses.is_empty() && !self.backed_by_shared_memory() {
                return Err(ValidationError::VhostUserRequiresSharedMemory);
            }
            for fs in fses {
                fs.validate(self)?;

                Self::validate_identifier(&mut id_list, &fs.pci_common.id)?;
            }
        }

        if let Some(generic_vhost_user_devices) = &self.generic_vhost_user {
            if !generic_vhost_user_devices.is_empty() && !self.backed_by_shared_memory() {
                return Err(ValidationError::VhostUserRequiresSharedMemory);
            }
            for generic_vhost_user_device in generic_vhost_user_devices {
                generic_vhost_user_device.validate(self)?;

                Self::validate_identifier(&mut id_list, &generic_vhost_user_device.pci_common.id)?;
            }
        }

        if let Some(pmems) = &self.pmem {
            for pmem in pmems {
                pmem.validate(self)?;
                self.iommu |= pmem.pci_common.iommu;

                Self::validate_identifier(&mut id_list, &pmem.pci_common.id)?;
            }
        }

        self.rng.validate(self)?;
        Self::validate_identifier(&mut id_list, &self.rng.pci_common.id)?;
        self.iommu |= self.rng.pci_common.iommu;

        if let Some(rtc) = &self.rtc {
            rtc.validate(self)?;
            Self::validate_identifier(&mut id_list, &rtc.pci_common.id)?;
            self.iommu |= rtc.pci_common.iommu;
        }

        self.console.validate(self)?;
        Self::validate_identifier(&mut id_list, &self.console.pci_common.id)?;
        self.iommu |= self.console.pci_common.iommu;

        if let Some(t) = &self.cpus.topology {
            if t.threads_per_core == 0
                || t.cores_per_die == 0
                || t.dies_per_package == 0
                || t.packages == 0
            {
                return Err(ValidationError::CpuTopologyZeroPart);
            }

            #[cfg(target_arch = "x86_64")]
            if t.threads_per_core > 2 {
                return Err(ValidationError::CpuTopologyThreadsPerCore);
            }

            // The setting of dies doesn't apply on AArch64.
            // Only '1' value is accepted, so its impact on the vcpu topology
            // setting can be ignored.
            #[cfg(target_arch = "aarch64")]
            if t.dies_per_package != 1 {
                return Err(ValidationError::CpuTopologyDiesPerPackage);
            }

            let total: u32 = (t.threads_per_core as u32)
                * (t.cores_per_die as u32)
                * (t.dies_per_package as u32)
                * (t.packages as u32);
            if total != self.cpus.max_vcpus {
                return Err(ValidationError::CpuTopologyCount);
            }
        }

        if let Some(hugepage_size) = &self.memory.hugepage_size {
            if !self.memory.hugepages {
                return Err(ValidationError::HugePageSizeWithoutHugePages);
            }
            if !hugepage_size.is_power_of_two() {
                return Err(ValidationError::InvalidHugePageSize(*hugepage_size));
            }
        }

        if self.memory.shared && self.memory.mergeable {
            return Err(ValidationError::InvalidSharedMemoryWithMergeable);
        }

        if let Some(zones) = &self.memory.zones {
            for zone in zones {
                if zone.shared && zone.mergeable {
                    return Err(ValidationError::InvalidSharedMemoryWithMergeable);
                }
            }
        }

        if let Some(user_devices) = &self.user_devices {
            if !user_devices.is_empty() && !self.backed_by_shared_memory() {
                return Err(ValidationError::UserDevicesRequireSharedMemory);
            }

            for user_device in user_devices {
                user_device.validate(self)?;

                Self::validate_identifier(&mut id_list, &user_device.pci_common.id)?;
            }
        }

        if let Some(vdpa_devices) = &self.vdpa {
            for vdpa_device in vdpa_devices {
                vdpa_device.validate(self)?;
                self.iommu |= vdpa_device.pci_common.iommu;

                Self::validate_identifier(&mut id_list, &vdpa_device.pci_common.id)?;
            }
        }

        if let Some(vsock) = &self.vsock
            && [!0, 0, 1, 2].contains(&vsock.cid)
        {
            return Err(ValidationError::VsockSpecialCid(vsock.cid));
        }

        if let Some(balloon) = &self.balloon {
            balloon.validate(self)?;
            Self::validate_identifier(&mut id_list, &balloon.pci_common.id)?;
            self.iommu |= balloon.pci_common.iommu;

            let ram_size = self.memory.total_size();
            if balloon.size >= ram_size {
                return Err(ValidationError::BalloonLargerThanRam(
                    balloon.size,
                    ram_size,
                ));
            }
        }

        if let Some(devices) = &self.devices {
            let mut device_paths = BTreeSet::new();
            for device in devices {
                if let Some(path) = device.path.as_deref()
                    && !device_paths.insert(path.to_string_lossy())
                {
                    return Err(ValidationError::DuplicateDevicePath(
                        path.to_string_lossy().to_string(),
                    ));
                }

                device.validate(self)?;
                self.iommu |= device.pci_common.iommu;

                Self::validate_identifier(&mut id_list, &device.pci_common.id)?;
            }
        }

        if let Some(vsock) = &self.vsock {
            vsock.validate(self)?;
            self.iommu |= vsock.pci_common.iommu;

            Self::validate_identifier(&mut id_list, &vsock.pci_common.id)?;
        }

        let num_pci_segments = match &self.platform {
            Some(platform_config) => platform_config.num_pci_segments,
            None => 1,
        };
        if let Some(numa) = &self.numa {
            let mut used_numa_node_memory_zones = HashMap::new();
            let mut used_pci_segments = HashMap::new();
            for numa_node in numa.iter() {
                numa_node.validate()?;
                if let Some(memory_zones) = numa_node.memory_zones.clone() {
                    for memory_zone in memory_zones.iter() {
                        if used_numa_node_memory_zones.contains_key(memory_zone) {
                            return Err(ValidationError::MemoryZoneReused(
                                memory_zone.to_string(),
                                *used_numa_node_memory_zones.get(memory_zone).unwrap(),
                                numa_node.guest_numa_id,
                            ));
                        }
                        used_numa_node_memory_zones
                            .insert(memory_zone.to_string(), numa_node.guest_numa_id);
                    }
                }

                if let Some(pci_segments) = numa_node.pci_segments.clone() {
                    for pci_segment in pci_segments.iter() {
                        if *pci_segment >= num_pci_segments {
                            return Err(ValidationError::InvalidPciSegment(*pci_segment));
                        }
                        if *pci_segment == 0 && numa_node.guest_numa_id != 0 {
                            return Err(ValidationError::DefaultPciSegmentInvalidNode(
                                numa_node.guest_numa_id,
                            ));
                        }
                        if used_pci_segments.contains_key(pci_segment) {
                            return Err(ValidationError::PciSegmentReused(
                                *pci_segment,
                                *used_pci_segments.get(pci_segment).unwrap(),
                                numa_node.guest_numa_id,
                            ));
                        }
                        used_pci_segments.insert(*pci_segment, numa_node.guest_numa_id);
                    }
                }
            }
        }

        if let Some(zones) = &self.memory.zones {
            for zone in zones.iter() {
                let id = zone.id.clone();
                Self::validate_identifier(&mut id_list, &Some(id))?;
            }
        }

        if let Some(pci_segments) = &self.pci_segments {
            for pci_segment in pci_segments {
                pci_segment.validate(self)?;
            }
        }

        self.platform.as_ref().map(|p| p.validate()).transpose()?;
        self.iommu |= self
            .platform
            .as_ref()
            .map(|p| p.iommu_segments.is_some())
            .unwrap_or_default();

        if let Some(landlock_rules) = &self.landlock_rules {
            for landlock_rule in landlock_rules {
                landlock_rule.validate()?;
            }
        }
        #[cfg(feature = "ivshmem")]
        if let Some(ivshmem_config) = &self.ivshmem {
            ivshmem_config.validate(self)?;
            Self::validate_identifier(&mut id_list, &ivshmem_config.pci_common.id)?;
        }

        Ok(id_list)
    }

    pub fn parse(vm_params: VmParams) -> Result<Self> {
        let mut rate_limit_groups: Option<Box<[RateLimiterGroupConfig]>> = None;
        if let Some(rate_limit_group_list) = &vm_params.rate_limit_groups {
            let mut rate_limit_group_config_list = Vec::new();
            for item in rate_limit_group_list.iter() {
                let rate_limit_group_config = RateLimiterGroupConfig::parse(item)?;
                rate_limit_group_config_list.push(rate_limit_group_config);
            }
            rate_limit_groups = Some(rate_limit_group_config_list.into_boxed_slice());
        }

        let mut disks: Option<Vec<DiskConfig>> = None;
        if let Some(disk_list) = &vm_params.disks {
            let mut disk_config_list = Vec::new();
            for item in disk_list.iter() {
                let disk_config = DiskConfig::parse(item)?;
                disk_config_list.push(disk_config);
            }
            disks = Some(disk_config_list);
        }

        #[cfg(feature = "fw_cfg")]
        let fw_cfg_config = if let Some(fw_cfg_config_str) = vm_params.fw_cfg_config {
            let fw_cfg_config = FwCfgConfig::parse(fw_cfg_config_str)?;
            Some(fw_cfg_config)
        } else {
            None
        };

        let mut net: Option<Vec<NetConfig>> = None;
        if let Some(net_list) = &vm_params.net {
            let mut net_config_list = Vec::new();
            for item in net_list.iter() {
                let net_config = NetConfig::parse(item)?;
                net_config_list.push(net_config);
            }
            net = Some(net_config_list);
        }

        let rng = RngConfig::parse(vm_params.rng)?;

        let mut rtc: Option<RtcConfig> = None;
        if let Some(rtc_params) = &vm_params.rtc {
            rtc = Some(RtcConfig::parse(rtc_params)?);
        }

        let mut balloon: Option<BalloonConfig> = None;
        if let Some(balloon_params) = &vm_params.balloon {
            balloon = Some(BalloonConfig::parse(balloon_params)?);
        }

        #[cfg(feature = "pvmemcontrol")]
        let pvmemcontrol: Option<PvmemcontrolConfig> = vm_params
            .pvmemcontrol
            .then_some(PvmemcontrolConfig::default());

        let mut fs: Option<Vec<FsConfig>> = None;
        if let Some(fs_list) = &vm_params.fs {
            let mut fs_config_list = Vec::new();
            for item in fs_list.iter() {
                fs_config_list.push(FsConfig::parse(item)?);
            }
            fs = Some(fs_config_list);
        }

        let mut generic_vhost_user: Option<Vec<GenericVhostUserConfig>> = None;
        if let Some(generic_vhost_user_list) = &vm_params.generic_vhost_user {
            let mut generic_vhost_user_config_list = Vec::new();
            for item in generic_vhost_user_list.iter() {
                generic_vhost_user_config_list.push(GenericVhostUserConfig::parse(item)?);
            }
            generic_vhost_user = Some(generic_vhost_user_config_list);
        }

        let mut pmem: Option<Vec<PmemConfig>> = None;
        if let Some(pmem_list) = &vm_params.pmem {
            let mut pmem_config_list = Vec::new();
            for item in pmem_list.iter() {
                let pmem_config = PmemConfig::parse(item)?;
                pmem_config_list.push(pmem_config);
            }
            pmem = Some(pmem_config_list);
        }

        let console = ConsoleConfig::parse(vm_params.console)?;
        let serial = SerialConfig::parse(vm_params.serial)?;
        #[cfg(target_arch = "x86_64")]
        let debug_console = DebugConsoleConfig::parse(vm_params.debug_console)?;

        let mut devices: Option<Vec<DeviceConfig>> = None;
        if let Some(device_list) = &vm_params.devices {
            let mut device_config_list = Vec::new();
            for item in device_list.iter() {
                let device_config = DeviceConfig::parse(item)?;
                device_config_list.push(device_config);
            }
            devices = Some(device_config_list);
        }

        let mut user_devices: Option<Vec<UserDeviceConfig>> = None;
        if let Some(user_device_list) = &vm_params.user_devices {
            let mut user_device_config_list = Vec::new();
            for item in user_device_list.iter() {
                let user_device_config = UserDeviceConfig::parse(item)?;
                user_device_config_list.push(user_device_config);
            }
            user_devices = Some(user_device_config_list);
        }

        let mut vdpa: Option<Vec<VdpaConfig>> = None;
        if let Some(vdpa_list) = &vm_params.vdpa {
            let mut vdpa_config_list = Vec::new();
            for item in vdpa_list.iter() {
                let vdpa_config = VdpaConfig::parse(item)?;
                vdpa_config_list.push(vdpa_config);
            }
            vdpa = Some(vdpa_config_list);
        }

        let mut vsock: Option<VsockConfig> = None;
        if let Some(vs) = &vm_params.vsock {
            let vsock_config = VsockConfig::parse(vs)?;
            vsock = Some(vsock_config);
        }

        let mut pci_segments: Option<Box<[PciSegmentConfig]>> = None;
        if let Some(pci_segment_list) = &vm_params.pci_segments {
            let mut pci_segment_config_list = Vec::new();
            for item in pci_segment_list.iter() {
                let pci_segment_config = PciSegmentConfig::parse(item)?;
                pci_segment_config_list.push(pci_segment_config);
            }
            pci_segments = Some(pci_segment_config_list.into_boxed_slice());
        }

        let platform = vm_params.platform.map(PlatformConfig::parse).transpose()?;

        let mut numa: Option<Box<[NumaConfig]>> = None;
        if let Some(numa_list) = &vm_params.numa {
            let mut numa_config_list = Vec::new();
            for item in numa_list.iter() {
                let numa_config = NumaConfig::parse(item)?;
                numa_config_list.push(numa_config);
            }
            numa = Some(numa_config_list.into_boxed_slice());
        }

        #[cfg(not(feature = "igvm"))]
        let payload_present = vm_params.kernel.is_some() || vm_params.firmware.is_some();

        #[cfg(feature = "igvm")]
        let payload_present =
            vm_params.kernel.is_some() || vm_params.firmware.is_some() || vm_params.igvm.is_some();

        let payload = if payload_present {
            Some(PayloadConfig {
                kernel: vm_params.kernel.map(PathBuf::from),
                initramfs: vm_params.initramfs.map(PathBuf::from),
                cmdline: vm_params.cmdline.map(|s| s.to_string()),
                firmware: vm_params.firmware.map(PathBuf::from),
                #[cfg(feature = "igvm")]
                igvm: vm_params.igvm.map(PathBuf::from),
                #[cfg(feature = "sev_snp")]
                host_data: vm_params.host_data.map(|s| s.to_string()),
                #[cfg(feature = "fw_cfg")]
                fw_cfg_config,
            })
        } else {
            None
        };

        let mut tpm: Option<TpmConfig> = None;
        if let Some(tc) = vm_params.tpm {
            let tpm_conf = TpmConfig::parse(tc)?;
            tpm = Some(TpmConfig {
                socket: tpm_conf.socket,
            });
        }

        #[cfg(feature = "guest_debug")]
        let gdb = vm_params.gdb;

        let mut landlock_rules: Option<Box<[LandlockConfig]>> = None;
        if let Some(ll_rules) = vm_params.landlock_rules {
            landlock_rules = Some(
                ll_rules
                    .iter()
                    .map(|rule| LandlockConfig::parse(rule))
                    .collect::<Result<Vec<LandlockConfig>>>()?
                    .into_boxed_slice(),
            );
        }

        #[cfg(feature = "ivshmem")]
        let mut ivshmem: Option<IvshmemConfig> = None;
        #[cfg(feature = "ivshmem")]
        if let Some(iv) = vm_params.ivshmem {
            let ivshmem_conf = IvshmemConfig::parse(iv)?;
            ivshmem = Some(ivshmem_conf);
        }

        let mut config = VmConfig {
            cpus: CpusConfig::parse(vm_params.cpus)?,
            memory: MemoryConfig::parse(vm_params.memory, vm_params.memory_zones)?,
            payload,
            rate_limit_groups,
            disks,
            net,
            rng,
            balloon,
            generic_vhost_user,
            fs,
            pmem,
            serial,
            console,
            #[cfg(target_arch = "x86_64")]
            debug_console,
            devices,
            user_devices,
            vdpa,
            vsock,
            #[cfg(feature = "pvmemcontrol")]
            pvmemcontrol,
            pvpanic: vm_params.pvpanic,
            iommu: false, // updated in VmConfig::validate()
            numa,
            watchdog: vm_params.watchdog,
            rtc,
            #[cfg(feature = "guest_debug")]
            gdb,
            pci_segments,
            platform,
            tpm,
            preserved_fds: None,
            landlock_enable: vm_params.landlock_enable,
            landlock_rules,
            #[cfg(feature = "ivshmem")]
            ivshmem,
        };
        config.validate().map_err(Error::Validation)?;
        Ok(config)
    }

    pub fn remove_device(&mut self, id: &str) -> bool {
        let mut removed = false;

        // Remove if VFIO device
        if let Some(devices) = self.devices.as_mut() {
            let len = devices.len();
            devices.retain(|dev| dev.pci_common.id.as_ref().map(|id| id.as_ref()) != Some(id));
            removed |= devices.len() != len;
        }

        // Remove if VFIO user device
        if let Some(user_devices) = self.user_devices.as_mut() {
            let len = user_devices.len();
            user_devices.retain(|dev| dev.pci_common.id.as_ref().map(|id| id.as_ref()) != Some(id));
            removed |= user_devices.len() != len;
        }

        // Remove if disk device
        if let Some(disks) = self.disks.as_mut() {
            let len = disks.len();
            disks.retain(|dev| dev.pci_common.id.as_ref().map(|id| id.as_ref()) != Some(id));
            removed |= disks.len() != len;
        }

        // Remove if fs device
        if let Some(fs) = self.fs.as_mut() {
            let len = fs.len();
            fs.retain(|dev| dev.pci_common.id.as_ref().map(|id| id.as_ref()) != Some(id));
            removed |= fs.len() != len;
        }

        // Remove if generic vhost-user device
        if let Some(generic_vhost_user) = self.generic_vhost_user.as_mut() {
            let len = generic_vhost_user.len();
            generic_vhost_user
                .retain(|dev| dev.pci_common.id.as_ref().map(|id| id.as_ref()) != Some(id));
            removed |= generic_vhost_user.len() != len;
        }

        // Remove if net device
        if let Some(net) = self.net.as_mut() {
            let len = net.len();
            net.retain(|dev| dev.pci_common.id.as_ref().map(|id| id.as_ref()) != Some(id));
            removed |= net.len() != len;
        }

        // Remove if pmem device
        if let Some(pmem) = self.pmem.as_mut() {
            let len = pmem.len();
            pmem.retain(|dev| dev.pci_common.id.as_ref().map(|id| id.as_ref()) != Some(id));
            removed |= pmem.len() != len;
        }

        // Remove if vDPA device
        if let Some(vdpa) = self.vdpa.as_mut() {
            let len = vdpa.len();
            vdpa.retain(|dev| dev.pci_common.id.as_ref().map(|id| id.as_ref()) != Some(id));
            removed |= vdpa.len() != len;
        }

        // Remove if vsock device
        if let Some(vsock) = self.vsock.as_ref()
            && vsock.pci_common.id.as_ref().map(|id| id.as_ref()) == Some(id)
        {
            self.vsock = None;
            removed = true;
        }

        removed
    }

    /// # Safety
    /// To use this safely, the caller must guarantee that the input
    /// fds are all valid.
    pub unsafe fn add_preserved_fds(&mut self, fds: Vec<i32>) {
        if fds.is_empty() {
            return;
        }

        self.preserved_fds
            .get_or_insert_with(HashSet::new)
            .extend(fds);
    }

    #[cfg(feature = "tdx")]
    pub fn is_tdx_enabled(&self) -> bool {
        self.platform.as_ref().is_some_and(|p| p.tdx)
    }

    #[cfg(feature = "sev_snp")]
    pub fn is_sev_snp_enabled(&self) -> bool {
        self.platform.as_ref().is_some_and(|p| p.sev_snp)
    }
}

impl Clone for VmConfig {
    fn clone(&self) -> Self {
        VmConfig {
            cpus: self.cpus.clone(),
            memory: self.memory.clone(),
            payload: self.payload.clone(),
            rate_limit_groups: self.rate_limit_groups.clone(),
            disks: self.disks.clone(),
            net: self.net.clone(),
            rng: self.rng.clone(),
            rtc: self.rtc.clone(),
            balloon: self.balloon.clone(),
            #[cfg(feature = "pvmemcontrol")]
            pvmemcontrol: self.pvmemcontrol.clone(),
            fs: self.fs.clone(),
            generic_vhost_user: self.generic_vhost_user.clone(),
            pmem: self.pmem.clone(),
            serial: self.serial.clone(),
            console: self.console.clone(),
            #[cfg(target_arch = "x86_64")]
            debug_console: self.debug_console.clone(),
            devices: self.devices.clone(),
            user_devices: self.user_devices.clone(),
            vdpa: self.vdpa.clone(),
            vsock: self.vsock.clone(),
            numa: self.numa.clone(),
            pci_segments: self.pci_segments.clone(),
            platform: self.platform.clone(),
            tpm: self.tpm.clone(),
            preserved_fds: self
                .preserved_fds
                .as_ref()
                // SAFETY: FFI call with valid FDs
                .map(|fds| fds.iter().map(|fd| unsafe { libc::dup(*fd) }).collect()),
            landlock_rules: self.landlock_rules.clone(),
            #[cfg(feature = "ivshmem")]
            ivshmem: self.ivshmem.clone(),
            ..*self
        }
    }
}

impl Drop for VmConfig {
    fn drop(&mut self) {
        if let Some(mut fds) = self.preserved_fds.take() {
            for fd in fds.drain() {
                // SAFETY: FFI call with valid FDs
                unsafe { libc::close(fd) };
            }
        }
    }
}

#[cfg(test)]
mod unit_tests {
    use std::fs::File;
    use std::os::unix::io::AsRawFd;

    use net_util::MacAddr;

    use super::*;

    #[test]
    fn test_cpu_parsing() -> Result<()> {
        assert_eq!(CpusConfig::parse("")?, CpusConfig::default());

        assert_eq!(
            CpusConfig::parse("boot=1")?,
            CpusConfig {
                boot_vcpus: 1,
                max_vcpus: 1,
                ..Default::default()
            }
        );
        assert_eq!(
            CpusConfig::parse("boot=1,max=2")?,
            CpusConfig {
                boot_vcpus: 1,
                max_vcpus: 2,
                ..Default::default()
            }
        );
        assert_eq!(
            CpusConfig::parse("boot=8,topology=2:2:1:2")?,
            CpusConfig {
                boot_vcpus: 8,
                max_vcpus: 8,
                topology: Some(CpuTopology {
                    threads_per_core: 2,
                    cores_per_die: 2,
                    dies_per_package: 1,
                    packages: 2
                }),
                ..Default::default()
            }
        );

        CpusConfig::parse("boot=8,topology=2:2:1").unwrap_err();
        CpusConfig::parse("boot=8,topology=2:2:1:x").unwrap_err();
        assert_eq!(
            CpusConfig::parse("boot=1,kvm_hyperv=on")?,
            CpusConfig {
                boot_vcpus: 1,
                max_vcpus: 1,
                kvm_hyperv: true,
                ..Default::default()
            }
        );
        assert_eq!(
            CpusConfig::parse("boot=2,affinity=[0@[0,2],1@[1,3]]")?,
            CpusConfig {
                boot_vcpus: 2,
                max_vcpus: 2,
                affinity: Some(Box::new([
                    CpuAffinity {
                        vcpu: 0,
                        host_cpus: Box::new([0, 2]),
                    },
                    CpuAffinity {
                        vcpu: 1,
                        host_cpus: Box::new([1, 3]),
                    }
                ])),
                ..Default::default()
            },
        );

        // Test core_scheduling parsing
        assert_eq!(
            CpusConfig::parse("boot=1,core_scheduling=vm")?,
            CpusConfig {
                boot_vcpus: 1,
                max_vcpus: 1,
                core_scheduling: CoreScheduling::Vm,
                ..Default::default()
            }
        );
        assert_eq!(
            CpusConfig::parse("boot=1,core_scheduling=vcpu")?,
            CpusConfig {
                boot_vcpus: 1,
                max_vcpus: 1,
                core_scheduling: CoreScheduling::Vcpu,
                ..Default::default()
            }
        );
        assert_eq!(
            CpusConfig::parse("boot=1,core_scheduling=off")?,
            CpusConfig {
                boot_vcpus: 1,
                max_vcpus: 1,
                core_scheduling: CoreScheduling::Off,
                ..Default::default()
            }
        );
        // Default (no core_scheduling specified) should be Vm
        assert_eq!(
            CpusConfig::parse("boot=1")?.core_scheduling,
            CoreScheduling::Vm
        );
        // Invalid value should error
        CpusConfig::parse("boot=1,core_scheduling=invalid").unwrap_err();

        Ok(())
    }

    #[test]
    fn test_mem_zone_parsing() -> Result<()> {
        // mergeable defaults to false
        assert_eq!(
            MemoryConfig::parse("size=0", Some(vec!["id=mem0,size=1G"]))?,
            MemoryConfig {
                size: 0,
                zones: Some(vec![MemoryZoneConfig {
                    id: "mem0".to_string(),
                    size: 1 << 30,
                    ..Default::default()
                }]),
                ..Default::default()
            }
        );
        // mergeable=on
        assert_eq!(
            MemoryConfig::parse("size=0", Some(vec!["id=mem0,size=1G,mergeable=on"]))?,
            MemoryConfig {
                size: 0,
                zones: Some(vec![MemoryZoneConfig {
                    id: "mem0".to_string(),
                    size: 1 << 30,
                    mergeable: true,
                    ..Default::default()
                }]),
                ..Default::default()
            }
        );
        // mergeable=off is explicit false
        assert_eq!(
            MemoryConfig::parse("size=0", Some(vec!["id=mem0,size=1G,mergeable=off"]))?,
            MemoryConfig {
                size: 0,
                zones: Some(vec![MemoryZoneConfig {
                    id: "mem0".to_string(),
                    size: 1 << 30,
                    mergeable: false,
                    ..Default::default()
                }]),
                ..Default::default()
            }
        );
        // per-zone mergeable independent of global mergeable
        assert_eq!(
            MemoryConfig::parse(
                "size=1G,mergeable=off",
                Some(vec!["id=hotplug,size=0,hotplug_size=4G,mergeable=on"])
            )?,
            MemoryConfig {
                size: 1 << 30,
                mergeable: false,
                hotplug_method: HotplugMethod::Acpi,
                zones: Some(vec![MemoryZoneConfig {
                    id: "hotplug".to_string(),
                    size: 0,
                    hotplug_size: Some(4 << 30),
                    mergeable: true,
                    ..Default::default()
                }]),
                ..Default::default()
            }
        );
        // global mergeable=on inherited by zone with no explicit mergeable
        assert_eq!(
            MemoryConfig::parse("size=0,mergeable=on", Some(vec!["id=mem0,size=1G"]))?,
            MemoryConfig {
                size: 0,
                mergeable: true,
                zones: Some(vec![MemoryZoneConfig {
                    id: "mem0".to_string(),
                    size: 1 << 30,
                    mergeable: true,
                    ..Default::default()
                }]),
                ..Default::default()
            }
        );
        // reserve=on on a zone
        assert_eq!(
            MemoryConfig::parse("size=0", Some(vec!["id=mem0,size=1G,reserve=on"]))?,
            MemoryConfig {
                size: 0,
                zones: Some(vec![MemoryZoneConfig {
                    id: "mem0".to_string(),
                    size: 1 << 30,
                    reserve: true,
                    ..Default::default()
                }]),
                ..Default::default()
            }
        );
        Ok(())
    }

    #[test]
    fn test_mem_parsing() -> Result<()> {
        assert_eq!(MemoryConfig::parse("", None)?, MemoryConfig::default());
        // Default string
        assert_eq!(
            MemoryConfig::parse("size=512M", None)?,
            MemoryConfig::default()
        );
        assert_eq!(
            MemoryConfig::parse("size=512M,mergeable=on", None)?,
            MemoryConfig {
                size: 512 << 20,
                mergeable: true,
                ..Default::default()
            }
        );
        assert_eq!(
            MemoryConfig::parse("mergeable=on", None)?,
            MemoryConfig {
                mergeable: true,
                ..Default::default()
            }
        );
        assert_eq!(
            MemoryConfig::parse("size=1G,mergeable=off", None)?,
            MemoryConfig {
                size: 1 << 30,
                mergeable: false,
                ..Default::default()
            }
        );
        assert_eq!(
            MemoryConfig::parse("hotplug_method=acpi", None)?,
            MemoryConfig {
                ..Default::default()
            }
        );
        assert_eq!(
            MemoryConfig::parse("hotplug_method=acpi,hotplug_size=512M", None)?,
            MemoryConfig {
                hotplug_size: Some(512 << 20),
                ..Default::default()
            }
        );
        assert_eq!(
            MemoryConfig::parse("hotplug_method=virtio-mem,hotplug_size=512M", None)?,
            MemoryConfig {
                hotplug_size: Some(512 << 20),
                hotplug_method: HotplugMethod::VirtioMem,
                ..Default::default()
            }
        );
        assert_eq!(
            MemoryConfig::parse("hugepages=on,size=1G,hugepage_size=2M", None)?,
            MemoryConfig {
                hugepage_size: Some(2 << 20),
                size: 1 << 30,
                hugepages: true,
                ..Default::default()
            }
        );
        // reserve=on opts out of MAP_NORESERVE
        assert_eq!(
            MemoryConfig::parse("size=1G,hugepages=on,reserve=on", None)?,
            MemoryConfig {
                size: 1 << 30,
                hugepages: true,
                reserve: true,
                ..Default::default()
            }
        );
        Ok(())
    }

    #[test]
    fn test_rate_limit_group_parsing() -> Result<()> {
        assert_eq!(
            RateLimiterGroupConfig::parse("id=group0,bw_size=1000,bw_refill_time=100")?,
            RateLimiterGroupConfig {
                id: "group0".to_string(),
                rate_limiter_config: RateLimiterConfig {
                    bandwidth: Some(TokenBucketConfig {
                        size: 1000,
                        one_time_burst: Some(0),
                        refill_time: 100,
                    }),
                    ops: None,
                }
            }
        );
        assert_eq!(
            RateLimiterGroupConfig::parse("id=group0,ops_size=1000,ops_refill_time=100")?,
            RateLimiterGroupConfig {
                id: "group0".to_string(),
                rate_limiter_config: RateLimiterConfig {
                    bandwidth: None,
                    ops: Some(TokenBucketConfig {
                        size: 1000,
                        one_time_burst: Some(0),
                        refill_time: 100,
                    }),
                }
            }
        );
        Ok(())
    }

    #[test]
    fn test_pci_segment_parsing() -> Result<()> {
        assert_eq!(
            PciSegmentConfig::parse("pci_segment=0")?,
            PciSegmentConfig {
                pci_segment: 0,
                mmio32_aperture_weight: 1,
                mmio64_aperture_weight: 1,
            }
        );
        assert_eq!(
            PciSegmentConfig::parse(
                "pci_segment=0,mmio32_aperture_weight=1,mmio64_aperture_weight=1"
            )?,
            PciSegmentConfig {
                pci_segment: 0,
                mmio32_aperture_weight: 1,
                mmio64_aperture_weight: 1,
            }
        );
        assert_eq!(
            PciSegmentConfig::parse("pci_segment=0,mmio32_aperture_weight=2")?,
            PciSegmentConfig {
                pci_segment: 0,
                mmio32_aperture_weight: 2,
                mmio64_aperture_weight: 1,
            }
        );
        assert_eq!(
            PciSegmentConfig::parse("pci_segment=0,mmio64_aperture_weight=2")?,
            PciSegmentConfig {
                pci_segment: 0,
                mmio32_aperture_weight: 1,
                mmio64_aperture_weight: 2,
            }
        );

        Ok(())
    }

    fn disk_fixture() -> DiskConfig {
        DiskConfig {
            pci_common: PciDeviceCommonConfig::default(),
            path: Some(PathBuf::from("/path/to_file")),
            readonly: false,
            direct: false,
            num_queues: 1,
            queue_size: 128,
            vhost_user: false,
            vhost_socket: None,
            disable_io_uring: false,
            disable_aio: false,
            rate_limit_group: None,
            rate_limiter_config: None,
            serial: None,
            queue_affinity: None,
            backing_files: false,
            sparse: true,
            image_type: ImageType::Unknown,
            lock_granularity: LockGranularityChoice::default(),
        }
    }

    #[test]
    fn test_disk_parsing() -> Result<()> {
        assert_eq!(
            DiskConfig::parse("path=/path/to_file")?,
            DiskConfig { ..disk_fixture() }
        );
        assert_eq!(
            DiskConfig::parse("path=/path/to_file,id=mydisk0")?,
            DiskConfig {
                pci_common: PciDeviceCommonConfig {
                    id: Some("mydisk0".to_owned()),
                    ..Default::default()
                },
                ..disk_fixture()
            }
        );
        assert_eq!(
            DiskConfig::parse("vhost_user=true,socket=/tmp/sock")?,
            DiskConfig {
                path: None,
                vhost_socket: Some(String::from("/tmp/sock")),
                vhost_user: true,
                image_type: ImageType::Unknown,
                ..disk_fixture()
            }
        );
        assert_eq!(
            DiskConfig::parse("path=/path/to_file,iommu=on")?,
            DiskConfig {
                pci_common: PciDeviceCommonConfig {
                    iommu: true,
                    ..Default::default()
                },
                ..disk_fixture()
            }
        );
        assert_eq!(
            DiskConfig::parse("path=/path/to_file,iommu=on,queue_size=256")?,
            DiskConfig {
                pci_common: PciDeviceCommonConfig {
                    iommu: true,
                    ..Default::default()
                },
                queue_size: 256,
                ..disk_fixture()
            }
        );
        assert_eq!(
            DiskConfig::parse("path=/path/to_file,iommu=on,queue_size=256,num_queues=4")?,
            DiskConfig {
                pci_common: PciDeviceCommonConfig {
                    iommu: true,
                    ..Default::default()
                },
                queue_size: 256,
                num_queues: 4,
                ..disk_fixture()
            }
        );
        assert_eq!(
            DiskConfig::parse("path=/path/to_file,direct=on")?,
            DiskConfig {
                direct: true,
                ..disk_fixture()
            }
        );
        assert_eq!(
            DiskConfig::parse("path=/path/to_file,serial=test")?,
            DiskConfig {
                serial: Some(String::from("test")),
                ..disk_fixture()
            }
        );
        assert_eq!(
            DiskConfig::parse("path=/path/to_file,rate_limit_group=group0")?,
            DiskConfig {
                rate_limit_group: Some("group0".to_string()),
                ..disk_fixture()
            }
        );
        assert_eq!(
            DiskConfig::parse("path=/path/to_file,lock_granularity=full")?,
            DiskConfig {
                lock_granularity: LockGranularityChoice::Full,
                ..disk_fixture()
            }
        );
        assert_eq!(
            DiskConfig::parse("path=/path/to_file,lock_granularity=byte-range")?,
            DiskConfig {
                lock_granularity: LockGranularityChoice::ByteRange,
                ..disk_fixture()
            }
        );
        assert_eq!(
            DiskConfig::parse("path=/path/to_file,queue_affinity=[0@[1],1@[2],2@[3,4],3@[5-8]]")?,
            DiskConfig {
                queue_affinity: Some(Box::new([
                    VirtQueueAffinity {
                        queue_index: 0,
                        host_cpus: Box::new([1]),
                    },
                    VirtQueueAffinity {
                        queue_index: 1,
                        host_cpus: Box::new([2]),
                    },
                    VirtQueueAffinity {
                        queue_index: 2,
                        host_cpus: Box::new([3, 4]),
                    },
                    VirtQueueAffinity {
                        queue_index: 3,
                        host_cpus: Box::new([5, 6, 7, 8]),
                    }
                ])),
                ..disk_fixture()
            }
        );
        Ok(())
    }

    fn net_fixture() -> NetConfig {
        NetConfig {
            pci_common: PciDeviceCommonConfig::default(),
            tap: None,
            ip: None,
            mask: None,
            mac: MacAddr::parse_str("de:ad:be:ef:12:34").unwrap(),
            host_mac: Some(MacAddr::parse_str("12:34:de:ad:be:ef").unwrap()),
            mtu: None,
            num_queues: 2,
            queue_size: 256,
            vhost_user: false,
            vhost_socket: None,
            vhost_mode: VhostMode::Client,
            fds: None,
            rate_limiter_config: None,
            offload_tso: true,
            offload_ufo: true,
            offload_csum: true,
        }
    }

    #[test]
    fn test_net_parsing() -> Result<()> {
        // mac address is random
        assert_eq!(
            NetConfig::parse("mac=de:ad:be:ef:12:34,host_mac=12:34:de:ad:be:ef")?,
            net_fixture(),
        );

        assert_eq!(
            NetConfig::parse("mac=de:ad:be:ef:12:34,host_mac=12:34:de:ad:be:ef,id=mynet0")?,
            NetConfig {
                pci_common: PciDeviceCommonConfig {
                    id: Some("mynet0".to_owned()),
                    ..Default::default()
                },
                ..net_fixture()
            }
        );

        assert_eq!(
            NetConfig::parse(
                "mac=de:ad:be:ef:12:34,host_mac=12:34:de:ad:be:ef,tap=tap0,ip=192.168.100.1,mask=255.255.255.128"
            )?,
            NetConfig {
                tap: Some("tap0".to_owned()),
                ip: Some("192.168.100.1".parse().unwrap()),
                mask: Some("255.255.255.128".parse().unwrap()),
                ..net_fixture()
            }
        );

        assert_eq!(
            NetConfig::parse(
                "mac=de:ad:be:ef:12:34,host_mac=12:34:de:ad:be:ef,vhost_user=true,socket=/tmp/sock"
            )?,
            NetConfig {
                vhost_user: true,
                vhost_socket: Some("/tmp/sock".to_owned()),
                ..net_fixture()
            }
        );

        assert_eq!(
            NetConfig::parse(
                "mac=de:ad:be:ef:12:34,host_mac=12:34:de:ad:be:ef,num_queues=4,queue_size=1024,iommu=on"
            )?,
            NetConfig {
                pci_common: PciDeviceCommonConfig {
                    iommu: true,
                    ..Default::default()
                },
                num_queues: 4,
                queue_size: 1024,
                ..net_fixture()
            }
        );

        assert_eq!(
            NetConfig::parse("mac=de:ad:be:ef:12:34,fd=[3,7],num_queues=4")?,
            NetConfig {
                host_mac: None,
                fds: Some(vec![3, 7]),
                num_queues: 4,
                ..net_fixture()
            }
        );

        assert_eq!(
            NetConfig::parse("mac=de:ad:be:ef:12:34,mask=255.255.255.0")?,
            NetConfig {
                mask: Some("255.255.255.0".parse().unwrap()),
                host_mac: None,
                ..net_fixture()
            }
        );

        Ok(())
    }

    #[test]
    fn test_parse_rng() -> Result<()> {
        assert_eq!(RngConfig::parse("")?, RngConfig::default());
        assert_eq!(
            RngConfig::parse("src=/dev/random")?,
            RngConfig {
                src: PathBuf::from("/dev/random"),
                ..Default::default()
            }
        );
        assert_eq!(
            RngConfig::parse("src=/dev/random,iommu=on,pci_segment=1,pci_device_id=7")?,
            RngConfig {
                src: PathBuf::from("/dev/random"),
                pci_common: PciDeviceCommonConfig {
                    id: None,
                    iommu: true,
                    pci_segment: 1,
                    pci_device_id: Some(7),
                },
            }
        );
        assert_eq!(
            RngConfig::parse("iommu=on")?,
            RngConfig {
                pci_common: PciDeviceCommonConfig {
                    id: None,
                    iommu: true,
                    pci_segment: 0,
                    pci_device_id: None,
                },
                ..Default::default()
            }
        );
        Ok(())
    }

    #[test]
    fn test_parse_balloon() -> Result<()> {
        assert_eq!(
            BalloonConfig::parse(
                "size=128M,deflate_on_oom=on,free_page_reporting=on,pci_segment=1,pci_device_id=7"
            )?,
            BalloonConfig {
                pci_common: PciDeviceCommonConfig {
                    pci_segment: 1,
                    pci_device_id: Some(7),
                    ..Default::default()
                },
                size: 128 << 20,
                deflate_on_oom: true,
                free_page_reporting: true,
            }
        );

        assert_eq!(
            BalloonConfig::parse("size=0,iommu=on")?,
            BalloonConfig {
                pci_common: PciDeviceCommonConfig {
                    iommu: true,
                    ..Default::default()
                },
                size: 0,
                deflate_on_oom: false,
                free_page_reporting: false,
            }
        );

        Ok(())
    }

    #[test]
    #[cfg(feature = "ivshmem")]
    fn test_parse_ivshmem() -> Result<()> {
        assert_eq!(
            IvshmemConfig::parse("path=/tmp/ivshmem.data,size=2M,pci_segment=1,pci_device_id=7")?,
            IvshmemConfig {
                pci_common: PciDeviceCommonConfig {
                    pci_segment: 1,
                    pci_device_id: Some(7),
                    ..Default::default()
                },
                path: PathBuf::from("/tmp/ivshmem.data"),
                size: 2 << 20,
            }
        );

        Ok(())
    }

    fn fs_fixture() -> FsConfig {
        FsConfig {
            pci_common: PciDeviceCommonConfig::default(),
            socket: PathBuf::from("/tmp/sock"),
            tag: "mytag".to_owned(),
            num_queues: 1,
            queue_size: 1024,
        }
    }

    #[test]
    fn test_parse_fs() -> Result<()> {
        // "tag" and "socket" must be supplied
        FsConfig::parse("").unwrap_err();
        FsConfig::parse("tag=mytag").unwrap_err();
        FsConfig::parse("socket=/tmp/sock").unwrap_err();
        assert_eq!(FsConfig::parse("tag=mytag,socket=/tmp/sock")?, fs_fixture());
        assert_eq!(
            FsConfig::parse("tag=mytag,socket=/tmp/sock,num_queues=4,queue_size=1024")?,
            FsConfig {
                num_queues: 4,
                queue_size: 1024,
                ..fs_fixture()
            }
        );

        Ok(())
    }

    #[track_caller]
    fn make_vhost_user_config(
        socket: &str,
        virtio_id: u64,
        id: &str,
        pci_segment: u64,
        queue_sizes: &IntegerList,
    ) {
        assert!(!socket.contains(",[]\n\r\0\""));
        assert!(!id.contains(",[]\n\r\0\""));
        let config = GenericVhostUserConfig::parse(&format!(
            "device_type={virtio_id},socket=\"{socket}\",\
id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}"
        ));
        if pci_segment <= u16::MAX.into()
            && virtio_id <= u32::MAX.into()
            && virtio_id != u64::from(VIRTIO_ID_BALLOON)
            && virtio_id != u64::from(VIRTIO_ID_WATCHDOG)
            && virtio_id != u64::from(VIRTIO_ID_IOMMU)
            && queue_sizes.0.iter().all(|&f| f <= u16::MAX.into())
        {
            assert_eq!(
                config.unwrap(),
                GenericVhostUserConfig {
                    pci_common: PciDeviceCommonConfig {
                        id: Some(id.to_owned()),
                        pci_segment: u16::try_from(pci_segment).unwrap(),
                        ..Default::default()
                    },
                    socket: socket.into(),
                    device_type: u32::try_from(virtio_id).unwrap(),
                    queue_sizes: queue_sizes
                        .0
                        .iter()
                        .map(|&f| u16::try_from(f).unwrap())
                        .collect(),
                }
            );
        } else {
            config.unwrap_err();
        }
    }

    #[test]
    fn test_parse_vhost_user() -> Result<()> {
        // all parameters must be supplied, except pci_segment
        GenericVhostUserConfig::parse("").unwrap_err();
        GenericVhostUserConfig::parse("virtio_id=1").unwrap_err();
        GenericVhostUserConfig::parse("queue_size=1").unwrap_err();
        GenericVhostUserConfig::parse("socket=/tmp/sock").unwrap_err();
        GenericVhostUserConfig::parse("id=1").unwrap_err();
        make_vhost_user_config(
            "/dev/null/doesnotexist",
            100,
            "Something",
            10,
            &IntegerList(vec![u16::MAX.into(), 20u16.into()]),
        );
        make_vhost_user_config(
            "/dev/null/doesnotexist",
            100,
            "Something",
            10,
            &IntegerList(vec![u16::MAX.into()]),
        );
        make_vhost_user_config(
            "/dev/null/doesnotexist",
            u64::from(u32::MAX) + 1,
            "Something",
            10,
            &IntegerList(vec![20u64]),
        );
        make_vhost_user_config(
            "/dev/null/doesnotexist",
            u64::from(u32::MAX) + 1,
            "Something",
            10,
            &IntegerList(vec![20u64]),
        );
        make_vhost_user_config(
            "/dev/null/doesnotexist",
            u64::from(u32::MAX) + 1,
            "Something",
            10,
            &IntegerList(vec![20u64]),
        );

        // The deprecated 'virtio_id' key is an alias for 'device_type' and must
        // parse to an identical configuration.
        assert_eq!(
            GenericVhostUserConfig::parse("virtio_id=26,socket=/tmp/sock,queue_sizes=[1024]")
                .unwrap(),
            GenericVhostUserConfig::parse("device_type=26,socket=/tmp/sock,queue_sizes=[1024]")
                .unwrap(),
        );
        Ok(())
    }

    fn pmem_fixture() -> PmemConfig {
        PmemConfig {
            pci_common: PciDeviceCommonConfig::default(),
            file: PathBuf::from("/tmp/pmem"),
            size: Some(128 << 20),
            discard_writes: false,
        }
    }

    #[test]
    fn test_pmem_parsing() -> Result<()> {
        // Must always give a file and size
        PmemConfig::parse("").unwrap_err();
        PmemConfig::parse("size=128M").unwrap_err();
        assert_eq!(
            PmemConfig::parse("file=/tmp/pmem,size=128M")?,
            pmem_fixture()
        );
        assert_eq!(
            PmemConfig::parse("file=/tmp/pmem,size=128M,id=mypmem0")?,
            PmemConfig {
                pci_common: PciDeviceCommonConfig {
                    id: Some("mypmem0".to_owned()),
                    ..Default::default()
                },
                ..pmem_fixture()
            }
        );
        assert_eq!(
            PmemConfig::parse("file=/tmp/pmem,size=128M,iommu=on,discard_writes=on")?,
            PmemConfig {
                pci_common: PciDeviceCommonConfig {
                    iommu: true,
                    ..Default::default()
                },
                discard_writes: true,
                ..pmem_fixture()
            }
        );

        Ok(())
    }

    #[test]
    fn test_console_parsing() -> Result<()> {
        let console_config = |mode, file, socket, iommu| ConsoleConfig {
            common: CommonConsoleConfig { file, mode, socket },
            pci_common: PciDeviceCommonConfig {
                iommu,
                ..Default::default()
            },
        };

        ConsoleConfig::parse("").unwrap_err();
        ConsoleConfig::parse("badmode").unwrap_err();
        assert_eq!(
            ConsoleConfig::parse("off")?,
            console_config(ConsoleOutputMode::Off, None, None, false)
        );
        assert_eq!(
            ConsoleConfig::parse("pty")?,
            console_config(ConsoleOutputMode::Pty, None, None, false)
        );
        assert_eq!(
            ConsoleConfig::parse("tty")?,
            console_config(ConsoleOutputMode::Tty, None, None, false)
        );
        assert_eq!(
            ConsoleConfig::parse("null")?,
            console_config(ConsoleOutputMode::Null, None, None, false)
        );
        assert_eq!(
            ConsoleConfig::parse("file=/tmp/console")?,
            console_config(
                ConsoleOutputMode::File,
                Some(PathBuf::from("/tmp/console")),
                None,
                false
            )
        );
        assert_eq!(
            ConsoleConfig::parse("null,iommu=on")?,
            console_config(ConsoleOutputMode::Null, None, None, true)
        );
        assert_eq!(
            ConsoleConfig::parse("file=/tmp/console,iommu=on")?,
            console_config(
                ConsoleOutputMode::File,
                Some(PathBuf::from("/tmp/console")),
                None,
                true
            )
        );
        assert_eq!(
            ConsoleConfig::parse("socket=/tmp/serial.sock,iommu=on")?,
            console_config(
                ConsoleOutputMode::Socket,
                None,
                Some(PathBuf::from("/tmp/serial.sock")),
                true
            )
        );
        Ok(())
    }

    fn device_fixture() -> DeviceConfig {
        DeviceConfig {
            pci_common: PciDeviceCommonConfig::default(),
            path: Some(PathBuf::from("/path/to/device")),
            fd: None,
            x_nv_gpudirect_clique: None,
            x_exclude_mmap_bars: Vec::new(),
        }
    }

    #[test]
    fn test_device_parsing() -> Result<()> {
        // The parser itself is purely syntactic; the "path or fd is
        // required" rule is enforced by VmConfig::validate instead.
        assert_eq!(
            DeviceConfig::parse("")?,
            DeviceConfig {
                path: None,
                ..device_fixture()
            }
        );
        assert_eq!(
            DeviceConfig::parse("path=/path/to/device")?,
            device_fixture()
        );

        assert_eq!(
            DeviceConfig::parse("path=/path/to/device,iommu=on")?,
            DeviceConfig {
                pci_common: PciDeviceCommonConfig {
                    iommu: true,
                    ..Default::default()
                },
                ..device_fixture()
            }
        );

        assert_eq!(
            DeviceConfig::parse("path=/path/to/device,iommu=on,id=mydevice0")?,
            DeviceConfig {
                pci_common: PciDeviceCommonConfig {
                    id: Some("mydevice0".to_owned()),
                    iommu: true,
                    ..Default::default()
                },
                ..device_fixture()
            }
        );

        assert_eq!(
            DeviceConfig::parse("path=/path/to/device,x_exclude_mmap_bars=[2]")?,
            DeviceConfig {
                x_exclude_mmap_bars: vec![2],
                ..device_fixture()
            }
        );

        assert_eq!(
            DeviceConfig::parse("path=/path/to/device,x_exclude_mmap_bars=[0,2,5]")?,
            DeviceConfig {
                x_exclude_mmap_bars: vec![0, 2, 5],
                ..device_fixture()
            }
        );

        assert_eq!(
            DeviceConfig::parse("path=/path/to/device,x_exclude_mmap_bars=[6]")?,
            DeviceConfig {
                x_exclude_mmap_bars: vec![6],
                ..device_fixture()
            }
        );

        // `fd=` is accepted alongside or in place of `path=`; exclusivity
        // is enforced by DeviceConfig::validate, not by the parser.
        assert_eq!(
            DeviceConfig::parse("fd=7")?,
            DeviceConfig {
                path: None,
                fd: Some(7),
                ..device_fixture()
            }
        );
        assert_eq!(
            DeviceConfig::parse("path=/path/to/device,fd=7")?,
            DeviceConfig {
                fd: Some(7),
                ..device_fixture()
            }
        );
        // Non-integer fd fails at parse time.
        DeviceConfig::parse("fd=notanint").unwrap_err();

        Ok(())
    }

    fn vdpa_fixture() -> VdpaConfig {
        VdpaConfig {
            pci_common: PciDeviceCommonConfig::default(),
            path: PathBuf::from("/dev/vhost-vdpa"),
            num_queues: 1,
        }
    }

    #[test]
    fn test_vdpa_parsing() -> Result<()> {
        // path is required
        VdpaConfig::parse("").unwrap_err();
        assert_eq!(VdpaConfig::parse("path=/dev/vhost-vdpa")?, vdpa_fixture());
        assert_eq!(
            VdpaConfig::parse("path=/dev/vhost-vdpa,num_queues=2,id=my_vdpa")?,
            VdpaConfig {
                pci_common: PciDeviceCommonConfig {
                    id: Some("my_vdpa".to_owned()),
                    ..Default::default()
                },
                num_queues: 2,
                ..vdpa_fixture()
            }
        );
        Ok(())
    }

    #[test]
    fn test_tpm_parsing() -> Result<()> {
        // path is required
        TpmConfig::parse("").unwrap_err();
        assert_eq!(
            TpmConfig::parse("socket=/var/run/tpm.sock")?,
            TpmConfig {
                socket: PathBuf::from("/var/run/tpm.sock"),
            }
        );
        Ok(())
    }

    #[test]
    fn test_platform_iommufd_fd_parsing() -> Result<()> {
        // `iommufd_fd=N` alone implies `iommufd=on`.
        let p = PlatformConfig::parse("iommufd_fd=42")?;
        assert!(p.iommufd);
        assert_eq!(p.iommufd_fd, Some(42));

        // Explicit `iommufd=on,iommufd_fd=N` is the same.
        let p = PlatformConfig::parse("iommufd=on,iommufd_fd=42")?;
        assert!(p.iommufd);
        assert_eq!(p.iommufd_fd, Some(42));

        // No flags → both default to off.
        let p = PlatformConfig::parse("")?;
        assert!(!p.iommufd);
        assert_eq!(p.iommufd_fd, None);

        Ok(())
    }

    #[test]
    fn test_vsock_parsing() -> Result<()> {
        // socket and cid is required
        VsockConfig::parse("").unwrap_err();
        assert_eq!(
            VsockConfig::parse("socket=/tmp/sock,cid=3")?,
            VsockConfig {
                pci_common: PciDeviceCommonConfig::default(),
                cid: 3,
                socket: PathBuf::from("/tmp/sock"),
            }
        );
        assert_eq!(
            VsockConfig::parse("socket=/tmp/sock,cid=3,iommu=on")?,
            VsockConfig {
                pci_common: PciDeviceCommonConfig {
                    iommu: true,
                    ..Default::default()
                },
                cid: 3,
                socket: PathBuf::from("/tmp/sock"),
            }
        );
        Ok(())
    }

    #[test]
    fn test_numa_config_parsing() -> Result<()> {
        // Error when device_id and cpu/memory are present
        let invalid_input = "guest_numa_id=0,cpus=[0,1],distances=[0@25,1@20],\
                                device_id=vfio0,memory_zones=[mem1],pci_segments=[0]";
        NumaConfig::parse(invalid_input).unwrap_err();
        // Successful numa config parsing
        let standard_input = "guest_numa_id=1,cpus=[2,3],distances=[0@20],\
                                memory_zones=[mem0],pci_segments=[0]";
        let expected_standard = NumaConfig {
            guest_numa_id: 1,
            cpus: Some(Box::new([2, 3])),
            distances: Some(Box::new([NumaDistance {
                destination: 0,
                distance: 20,
            }])),
            device_id: None,
            memory_zones: Some(Box::new(["mem0".to_string()])),
            pci_segments: Some(Box::new([0])),
        };
        assert_eq!(NumaConfig::parse(standard_input)?, expected_standard);
        // Successful generic initiator config parse
        let gi_input = "guest_numa_id=2,device_id=vfio1,distances=[0@30],pci_segments=[1]";
        let expected_gi = NumaConfig {
            guest_numa_id: 2,
            cpus: None,
            distances: Some(Box::new([NumaDistance {
                destination: 0,
                distance: 30,
            }])),
            device_id: Some("vfio1".to_string()),
            memory_zones: None,
            pci_segments: Some(Box::new([1])),
        };
        assert_eq!(NumaConfig::parse(gi_input)?, expected_gi);
        Ok(())
    }

    #[test]
    fn test_numa_config_generic_initiator_valid() {
        // device_id specified, no cpus/memory_zones
        let config = NumaConfig {
            guest_numa_id: 0,
            cpus: None,
            distances: Some(Box::new([NumaDistance {
                destination: 1,
                distance: 20,
            }])),
            memory_zones: None,
            device_id: Some("vfio0".to_string()),
            pci_segments: None,
        };
        config.validate().unwrap();
        assert!(config.is_generic_initiator());
    }

    #[test]
    fn test_numa_config_invalid_device_id() {
        // empty device_id
        let config = NumaConfig {
            guest_numa_id: 0,
            cpus: None,
            distances: None,
            memory_zones: None,
            device_id: Some(String::new()),
            pci_segments: None,
        };
        assert!(config.validate().is_err());
    }

    #[test]
    fn test_numa_config_invalid_both_device_cpus() {
        // device_id and cpus specified
        let config = NumaConfig {
            guest_numa_id: 0,
            cpus: Some(Box::new([0, 1])),
            distances: None,
            device_id: Some("vfio0".to_string()),
            memory_zones: None,
            pci_segments: None,
        };
        assert!(config.validate().is_err());
    }

    #[test]
    fn test_numa_config_invalid_both_device_memory() {
        // device_id and memory zones specified
        let config = NumaConfig {
            guest_numa_id: 0,
            cpus: None,
            distances: None,
            device_id: Some("vfio0".to_string()),
            memory_zones: Some(Box::new(["mem0".to_string()])),
            pci_segments: None,
        };
        assert!(config.validate().is_err());
    }

    #[test]
    fn test_numa_config_standard_valid() {
        // No device_id
        let config = NumaConfig {
            guest_numa_id: 0,
            cpus: Some(Box::new([0, 1])),
            distances: Some(Box::new([NumaDistance {
                destination: 1,
                distance: 20,
            }])),
            device_id: None,
            memory_zones: Some(Box::new(["mem0".to_string()])),
            pci_segments: None,
        };
        config.validate().unwrap();
    }

    #[test]
    fn test_restore_parsing() -> Result<()> {
        assert_eq!(
            RestoreConfig::parse("source_url=/path/to/snapshot")?,
            RestoreConfig {
                source_url: PathBuf::from("/path/to/snapshot"),
                prefault: false,
                memory_restore_mode: MemoryRestoreMode::Copy,
                net_fds: None,
                resume: false,
            }
        );
        assert_eq!(
            RestoreConfig::parse(
                "source_url=/path/to/snapshot,prefault=off,net_fds=[net0@[3,4],net1@[5,6,7,8]]"
            )?,
            RestoreConfig {
                source_url: PathBuf::from("/path/to/snapshot"),
                prefault: false,
                memory_restore_mode: MemoryRestoreMode::Copy,
                net_fds: Some(vec![
                    RestoredNetConfig {
                        id: "net0".to_string(),
                        num_fds: 2,
                        fds: Some(vec![3, 4]),
                    },
                    RestoredNetConfig {
                        id: "net1".to_string(),
                        num_fds: 4,
                        fds: Some(vec![5, 6, 7, 8]),
                    }
                ]),
                resume: false,
            }
        );
        assert_eq!(
            RestoreConfig::parse("source_url=/path/to/snapshot,memory_restore_mode=ondemand")?,
            RestoreConfig {
                source_url: PathBuf::from("/path/to/snapshot"),
                prefault: false,
                memory_restore_mode: MemoryRestoreMode::OnDemand,
                net_fds: None,
                resume: false,
            }
        );
        assert_eq!(
            RestoreConfig::parse("source_url=/path/to/snapshot,resume=on")?,
            RestoreConfig {
                source_url: PathBuf::from("/path/to/snapshot"),
                prefault: false,
                memory_restore_mode: MemoryRestoreMode::Copy,
                net_fds: None,
                resume: true,
            }
        );
        // Parsing should fail as source_url is a required field
        RestoreConfig::parse("prefault=off").unwrap_err();
        RestoreConfig::parse("source_url=/path/to/snapshot,memory_restore_mode=bogus").unwrap_err();
        Ok(())
    }

    #[test]
    fn test_restore_config_serde() {
        assert_eq!(
            serde_json::from_str::<RestoreConfig>(r#"{"source_url":"/path/to/snapshot"}"#)
                .unwrap()
                .memory_restore_mode,
            MemoryRestoreMode::Copy
        );
        assert_eq!(
            serde_json::from_str::<RestoreConfig>(
                r#"{"source_url":"/path/to/snapshot","memory_restore_mode":"OnDemand"}"#
            )
            .unwrap()
            .memory_restore_mode,
            MemoryRestoreMode::OnDemand
        );
    }

    #[test]
    fn test_restore_config_validation() {
        // interested in only VmConfig.net, so set rest to default values
        let mut snapshot_vm_config = VmConfig {
            cpus: CpusConfig::default(),
            memory: MemoryConfig::default(),
            payload: None,
            rate_limit_groups: None,
            disks: None,
            rng: RngConfig::default(),
            generic_vhost_user: None,
            balloon: None,
            fs: None,
            pmem: None,
            serial: SerialConfig::default(),
            console: ConsoleConfig::default(),
            #[cfg(target_arch = "x86_64")]
            debug_console: DebugConsoleConfig::default(),
            devices: None,
            user_devices: None,
            vdpa: None,
            vsock: None,
            #[cfg(feature = "pvmemcontrol")]
            pvmemcontrol: None,
            pvpanic: false,
            iommu: false,
            numa: None,
            watchdog: false,
            rtc: None,
            #[cfg(feature = "guest_debug")]
            gdb: false,
            pci_segments: None,
            platform: None,
            tpm: None,
            preserved_fds: None,
            net: Some(vec![
                NetConfig {
                    pci_common: PciDeviceCommonConfig {
                        id: Some("net0".to_owned()),
                        ..Default::default()
                    },
                    num_queues: 2,
                    fds: Some(vec![-1, -1, -1, -1]),
                    ..net_fixture()
                },
                NetConfig {
                    pci_common: PciDeviceCommonConfig {
                        id: Some("net1".to_owned()),
                        ..Default::default()
                    },
                    num_queues: 1,
                    fds: Some(vec![-1, -1]),
                    ..net_fixture()
                },
                NetConfig {
                    pci_common: PciDeviceCommonConfig {
                        id: Some("net2".to_owned()),
                        ..Default::default()
                    },
                    fds: None,
                    ..net_fixture()
                },
            ]),
            landlock_enable: false,
            landlock_rules: None,
            #[cfg(feature = "ivshmem")]
            ivshmem: None,
        };

        let valid_config = RestoreConfig {
            source_url: PathBuf::from("/path/to/snapshot"),
            prefault: false,
            memory_restore_mode: MemoryRestoreMode::Copy,
            net_fds: Some(vec![
                RestoredNetConfig {
                    id: "net0".to_string(),
                    num_fds: 4,
                    fds: Some(vec![3, 4, 5, 6]),
                },
                RestoredNetConfig {
                    id: "net1".to_string(),
                    num_fds: 2,
                    fds: Some(vec![7, 8]),
                },
            ]),
            resume: false,
        };
        valid_config.validate(&snapshot_vm_config).unwrap();

        let mut invalid_config = valid_config.clone();
        invalid_config.net_fds = Some(vec![RestoredNetConfig {
            id: "netx".to_string(),
            num_fds: 4,
            fds: Some(vec![3, 4, 5, 6]),
        }]);
        assert_eq!(
            invalid_config.validate(&snapshot_vm_config),
            Err(ValidationError::RestoreMissingRequiredNetId(
                "net0".to_string()
            ))
        );

        invalid_config.net_fds = Some(vec![
            RestoredNetConfig {
                id: "net0".to_string(),
                num_fds: 4,
                fds: Some(vec![3, 4, 5, 6]),
            },
            RestoredNetConfig {
                id: "net0".to_string(),
                num_fds: 4,
                fds: Some(vec![3, 4, 5, 6]),
            },
        ]);
        assert_eq!(
            invalid_config.validate(&snapshot_vm_config),
            Err(ValidationError::IdentifierNotUnique("net0".to_string()))
        );

        invalid_config.net_fds = Some(vec![RestoredNetConfig {
            id: "net0".to_string(),
            num_fds: 4,
            fds: Some(vec![3, 4, 5, 6]),
        }]);
        assert_eq!(
            invalid_config.validate(&snapshot_vm_config),
            Err(ValidationError::RestoreMissingRequiredNetId(
                "net1".to_string()
            ))
        );

        invalid_config.net_fds = Some(vec![RestoredNetConfig {
            id: "net0".to_string(),
            num_fds: 2,
            fds: Some(vec![3, 4]),
        }]);
        assert_eq!(
            invalid_config.validate(&snapshot_vm_config),
            Err(ValidationError::RestoreNetFdCountMismatch(
                "net0".to_string(),
                2,
                4
            ))
        );

        let another_valid_config = RestoreConfig {
            source_url: PathBuf::from("/path/to/snapshot"),
            prefault: false,
            memory_restore_mode: MemoryRestoreMode::Copy,
            net_fds: None,
            resume: false,
        };
        snapshot_vm_config.net = Some(vec![NetConfig {
            pci_common: PciDeviceCommonConfig {
                id: Some("net2".to_owned()),
                ..Default::default()
            },
            fds: None,
            ..net_fixture()
        }]);
        another_valid_config.validate(&snapshot_vm_config).unwrap();

        let invalid_restore_mode = RestoreConfig {
            source_url: PathBuf::from("/path/to/snapshot"),
            prefault: true,
            memory_restore_mode: MemoryRestoreMode::OnDemand,
            net_fds: None,
            resume: false,
        };
        assert_eq!(
            invalid_restore_mode.validate(&snapshot_vm_config),
            Err(ValidationError::InvalidRestorePrefaultWithOnDemand)
        );
    }

    fn platform_fixture() -> PlatformConfig {
        PlatformConfig {
            num_pci_segments: MAX_NUM_PCI_SEGMENTS,
            iommu_segments: None,
            iommu_address_width_bits: MAX_IOMMU_ADDRESS_WIDTH_BITS,
            system_serial_number: None,
            system_uuid: None,
            oem_strings: None,
            iommufd: false,
            iommufd_fd: None,
            vfio_p2p_dma: default_platformconfig_vfio_p2p_dma(),
            system_manufacturer: None,
            system_product_name: None,
            system_version: None,
            system_family: None,
            system_sku_number: None,
            chassis_asset_tag: None,
            #[cfg(feature = "tdx")]
            tdx: false,
            #[cfg(feature = "sev_snp")]
            sev_snp: false,
        }
    }

    fn numa_fixture() -> NumaConfig {
        NumaConfig {
            guest_numa_id: 0,
            cpus: None,
            distances: None,
            device_id: None,
            memory_zones: None,
            pci_segments: None,
        }
    }

    #[test]
    fn test_config_validation() {
        let mut valid_config = VmConfig {
            cpus: CpusConfig {
                boot_vcpus: 1,
                max_vcpus: 1,
                ..Default::default()
            },
            memory: MemoryConfig {
                size: 536_870_912,
                mergeable: false,
                hotplug_method: HotplugMethod::Acpi,
                hotplug_size: None,
                hotplugged_size: None,
                shared: false,
                hugepages: false,
                hugepage_size: None,
                prefault: false,
                reserve: false,
                zones: None,
                thp: true,
            },
            payload: Some(PayloadConfig {
                kernel: Some(PathBuf::from("/path/to/kernel")),
                firmware: None,
                cmdline: None,
                initramfs: None,
                #[cfg(feature = "igvm")]
                igvm: None,
                #[cfg(feature = "sev_snp")]
                host_data: Some(
                    "243eb7dc1a21129caa91dcbb794922b933baecb5823a377eb431188673288c07".to_string(),
                ),
                #[cfg(feature = "fw_cfg")]
                fw_cfg_config: None,
            }),
            rate_limit_groups: None,
            disks: None,
            net: None,
            rng: RngConfig {
                src: PathBuf::from("/dev/urandom"),
                pci_common: PciDeviceCommonConfig::default(),
            },
            balloon: None,
            fs: None,
            generic_vhost_user: None,
            pmem: None,
            serial: SerialConfig {
                common: CommonConsoleConfig {
                    file: None,
                    mode: ConsoleOutputMode::Null,
                    socket: None,
                },
            },
            console: ConsoleConfig {
                common: CommonConsoleConfig {
                    file: None,
                    mode: ConsoleOutputMode::Tty,
                    socket: None,
                },
                pci_common: PciDeviceCommonConfig::default(),
            },
            #[cfg(target_arch = "x86_64")]
            debug_console: DebugConsoleConfig::default(),
            devices: None,
            user_devices: None,
            vdpa: None,
            vsock: None,
            #[cfg(feature = "pvmemcontrol")]
            pvmemcontrol: None,
            pvpanic: false,
            iommu: false,
            numa: None,
            watchdog: false,
            rtc: None,
            #[cfg(feature = "guest_debug")]
            gdb: false,
            pci_segments: None,
            platform: None,
            tpm: None,
            preserved_fds: None,
            landlock_enable: false,
            landlock_rules: None,
            #[cfg(feature = "ivshmem")]
            ivshmem: None,
        };

        valid_config.validate().unwrap();

        let mut invalid_config = valid_config.clone();
        invalid_config.serial.common.mode = ConsoleOutputMode::Tty;
        invalid_config.console.common.mode = ConsoleOutputMode::Tty;
        valid_config.validate().unwrap();

        let mut invalid_config = valid_config.clone();
        invalid_config.payload = None;
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::PayloadError(
                PayloadConfigError::MissingBootitem
            ))
        );

        #[cfg(feature = "fw_cfg")]
        {
            let mut invalid_config = valid_config.clone();
            if let Some(payload) = invalid_config.payload.as_mut() {
                payload.fw_cfg_config = Some(FwCfgConfig {
                    e820: true,
                    kernel: false,
                    cmdline: false,
                    initramfs: false,
                    acpi_tables: true,
                    items: Some(FwCfgItemList {
                        item_list: vec![FwCfgItem {
                            name: "opt/org.test/invalid".to_string(),
                            file: None,
                            string: None,
                        }],
                    }),
                });
            }
            assert_eq!(
                invalid_config.validate(),
                Err(ValidationError::PayloadError(
                    PayloadConfigError::FwCfgInvalidItem("opt/org.test/invalid".to_string())
                ))
            );
        }

        let mut invalid_config = valid_config.clone();
        invalid_config.serial.common.mode = ConsoleOutputMode::File;
        invalid_config.serial.common.file = None;
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::ConsoleFileMissing)
        );

        let mut invalid_config = valid_config.clone();
        invalid_config.cpus.max_vcpus = 16;
        invalid_config.cpus.boot_vcpus = 32;
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::CpusMaxLowerThanBoot(16, 32))
        );

        let mut invalid_config = valid_config.clone();
        invalid_config.cpus.max_vcpus = 16;
        invalid_config.cpus.boot_vcpus = 16;
        invalid_config.cpus.topology = Some(CpuTopology {
            threads_per_core: 2,
            cores_per_die: 8,
            dies_per_package: 1,
            packages: 2,
        });
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::CpuTopologyCount)
        );

        let mut still_valid_config = valid_config.clone();
        still_valid_config.cpus.max_vcpus = 8;
        still_valid_config.cpus.boot_vcpus = 8;
        still_valid_config.cpus.topology = Some(CpuTopology {
            threads_per_core: 1,
            cores_per_die: 8,
            dies_per_package: 1,
            packages: 1,
        });
        still_valid_config.validate().unwrap();

        let mut still_valid_config = valid_config.clone();
        still_valid_config.cpus.max_vcpus = 8;
        still_valid_config.cpus.boot_vcpus = 8;
        still_valid_config.cpus.topology = Some(CpuTopology {
            threads_per_core: 2,
            cores_per_die: 4,
            dies_per_package: 1,
            packages: 1,
        });
        still_valid_config.validate().unwrap();

        #[cfg(target_arch = "x86_64")]
        {
            let mut invalid_config = valid_config.clone();
            invalid_config.cpus.max_vcpus = 6;
            invalid_config.cpus.boot_vcpus = 6;
            invalid_config.cpus.topology = Some(CpuTopology {
                threads_per_core: 3,
                cores_per_die: 2,
                dies_per_package: 1,
                packages: 1,
            });
            assert_eq!(
                invalid_config.validate(),
                Err(ValidationError::CpuTopologyThreadsPerCore)
            );
        }

        let mut invalid_config = valid_config.clone();
        invalid_config.disks = Some(vec![DiskConfig {
            vhost_socket: Some("/path/to/sock".to_owned()),
            path: Some(PathBuf::from("/path/to/image")),
            ..disk_fixture()
        }]);
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::DiskSocketAndPath)
        );

        let mut invalid_config = valid_config.clone();
        invalid_config.memory.shared = true;
        invalid_config.disks = Some(vec![DiskConfig {
            path: None,
            vhost_user: true,
            ..disk_fixture()
        }]);
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::VhostUserMissingSocket)
        );

        let mut invalid_config = valid_config.clone();
        invalid_config.disks = Some(vec![DiskConfig {
            path: None,
            vhost_user: true,
            vhost_socket: Some("/path/to/sock".to_owned()),
            ..disk_fixture()
        }]);
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::VhostUserRequiresSharedMemory)
        );

        let mut still_valid_config = valid_config.clone();
        still_valid_config.disks = Some(vec![DiskConfig {
            path: None,
            vhost_user: true,
            vhost_socket: Some("/path/to/sock".to_owned()),
            ..disk_fixture()
        }]);
        still_valid_config.memory.shared = true;
        still_valid_config.validate().unwrap();

        // A block queue size that is not a power of 2 is rejected.
        let mut invalid_config = valid_config.clone();
        invalid_config.disks = Some(vec![DiskConfig {
            queue_size: 100,
            ..disk_fixture()
        }]);
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::InvalidQueueSize(100))
        );

        // A power-of-2 block queue size too small for a usable seg_max is
        // rejected with the block-specific error.
        let mut invalid_config = valid_config.clone();
        invalid_config.disks = Some(vec![DiskConfig {
            queue_size: MINIMUM_BLOCK_QUEUE_SIZE,
            ..disk_fixture()
        }]);
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::BlockQueueSizeTooSmall(
                MINIMUM_BLOCK_QUEUE_SIZE
            ))
        );

        // A net queue size that is not a power of 2 is rejected.
        let mut invalid_config = valid_config.clone();
        invalid_config.net = Some(vec![NetConfig {
            queue_size: 100,
            ..net_fixture()
        }]);
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::InvalidQueueSize(100))
        );

        let mut invalid_config = valid_config.clone();
        invalid_config.net = Some(vec![NetConfig {
            vhost_user: true,
            ..net_fixture()
        }]);
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::VhostUserRequiresSharedMemory)
        );

        let mut invalid_config = valid_config.clone();
        invalid_config.memory.shared = true;
        invalid_config.net = Some(vec![NetConfig {
            vhost_user: true,
            ..net_fixture()
        }]);
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::VhostUserMissingSocket)
        );

        let mut still_valid_config = valid_config.clone();
        still_valid_config.net = Some(vec![NetConfig {
            vhost_user: true,
            vhost_socket: Some("/path/to/sock".to_owned()),
            ..net_fixture()
        }]);
        still_valid_config.memory.shared = true;
        still_valid_config.validate().unwrap();

        // Test vhost_user with rate limiting for disk
        let mut invalid_config = valid_config.clone();
        invalid_config.memory.shared = true;
        invalid_config.disks = Some(vec![DiskConfig {
            path: None,
            vhost_user: true,
            vhost_socket: Some("/path/to/sock".to_owned()),
            rate_limiter_config: Some(RateLimiterConfig::default()),
            ..disk_fixture()
        }]);
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::VhostUserRateLimiterNotSupported)
        );

        // Test vhost_user with rate_limit_group for disk
        let mut invalid_config = valid_config.clone();
        invalid_config.memory.shared = true;
        invalid_config.disks = Some(vec![DiskConfig {
            path: None,
            vhost_user: true,
            vhost_socket: Some("/path/to/sock".to_owned()),
            rate_limit_group: Some("group0".to_string()),
            ..disk_fixture()
        }]);
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::VhostUserRateLimiterNotSupported)
        );

        // Test vhost_user with rate limiting for net
        let mut invalid_config = valid_config.clone();
        invalid_config.memory.shared = true;
        invalid_config.net = Some(vec![NetConfig {
            vhost_user: true,
            vhost_socket: Some("/path/to/sock".to_owned()),
            rate_limiter_config: Some(RateLimiterConfig::default()),
            ..net_fixture()
        }]);
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::VhostUserRateLimiterNotSupported)
        );

        let mut invalid_config = valid_config.clone();
        invalid_config.net = Some(vec![NetConfig {
            fds: Some(vec![0]),
            ..net_fixture()
        }]);
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::VnetReservedFd(0))
        );

        let mut invalid_config = valid_config.clone();
        invalid_config.net = Some(vec![NetConfig {
            offload_csum: false,
            ..net_fixture()
        }]);
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::NoHardwareChecksumOffload)
        );

        let mut invalid_config = valid_config.clone();
        invalid_config.net = Some(vec![NetConfig {
            ip: None,
            mask: Some("255.255.255.0".parse().unwrap()),
            ..net_fixture()
        }]);
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::MaskProvidedWithoutIp)
        );

        let mut invalid_config = valid_config.clone();
        invalid_config.net = Some(vec![NetConfig {
            ip: Some("192.1.33.7".parse().unwrap()),
            mask: None,
            ..net_fixture()
        }]);
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::IpProvidedWithoutMask)
        );

        let mut invalid_config = valid_config.clone();
        invalid_config.fs = Some(vec![fs_fixture()]);
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::VhostUserRequiresSharedMemory)
        );

        let mut still_valid_config = valid_config.clone();
        still_valid_config.memory.shared = true;
        still_valid_config.validate().unwrap();

        let mut still_valid_config = valid_config.clone();
        still_valid_config.memory.hugepages = true;
        still_valid_config.validate().unwrap();

        let mut still_valid_config = valid_config.clone();
        still_valid_config.memory.hugepages = true;
        still_valid_config.memory.hugepage_size = Some(2 << 20);
        still_valid_config.validate().unwrap();

        let mut invalid_config = valid_config.clone();
        invalid_config.memory.hugepages = false;
        invalid_config.memory.hugepage_size = Some(2 << 20);
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::HugePageSizeWithoutHugePages)
        );

        let mut invalid_config = valid_config.clone();
        invalid_config.memory.hugepages = true;
        invalid_config.memory.hugepage_size = Some(3 << 20);
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::InvalidHugePageSize(3 << 20))
        );

        // Test mergeable and shared validation for global memory
        let mut invalid_config = valid_config.clone();
        invalid_config.memory.shared = true;
        invalid_config.memory.mergeable = true;
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::InvalidSharedMemoryWithMergeable)
        );

        // Test mergeable and shared validation for memory zones
        let mut invalid_config = valid_config.clone();
        invalid_config.memory.zones = Some(vec![MemoryZoneConfig {
            id: "mem0".to_string(),
            size: 1 << 30,
            shared: true,
            mergeable: true,
            ..Default::default()
        }]);
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::InvalidSharedMemoryWithMergeable)
        );

        let mut still_valid_config = valid_config.clone();
        still_valid_config.platform = Some(platform_fixture());
        still_valid_config.validate().unwrap();

        let mut invalid_config = valid_config.clone();
        invalid_config.platform = Some(PlatformConfig {
            num_pci_segments: MAX_NUM_PCI_SEGMENTS + 1,
            ..platform_fixture()
        });
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::InvalidNumPciSegments(
                MAX_NUM_PCI_SEGMENTS + 1
            ))
        );

        let mut still_valid_config = valid_config.clone();
        still_valid_config.platform = Some(PlatformConfig {
            iommu_segments: Some(Box::new([1, 2, 3])),
            ..platform_fixture()
        });
        still_valid_config.validate().unwrap();

        let mut invalid_config = valid_config.clone();
        invalid_config.platform = Some(PlatformConfig {
            iommu_segments: Some(Box::new([
                MAX_NUM_PCI_SEGMENTS + 1,
                MAX_NUM_PCI_SEGMENTS + 2,
            ])),
            ..platform_fixture()
        });
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::InvalidPciSegment(MAX_NUM_PCI_SEGMENTS + 1))
        );

        let mut invalid_config = valid_config.clone();
        invalid_config.platform = Some(PlatformConfig {
            iommu_address_width_bits: MAX_IOMMU_ADDRESS_WIDTH_BITS + 1,
            ..platform_fixture()
        });
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::InvalidIommuAddressWidthBits(
                MAX_IOMMU_ADDRESS_WIDTH_BITS + 1
            ))
        );

        let mut still_valid_config = valid_config.clone();
        still_valid_config.platform = Some(PlatformConfig {
            iommu_segments: Some(Box::new([1, 2, 3])),
            ..platform_fixture()
        });
        still_valid_config.disks = Some(vec![DiskConfig {
            pci_common: PciDeviceCommonConfig {
                iommu: true,
                pci_segment: 1,
                ..Default::default()
            },
            ..disk_fixture()
        }]);
        still_valid_config.validate().unwrap();

        let mut still_valid_config = valid_config.clone();
        still_valid_config.platform = Some(PlatformConfig {
            iommu_segments: Some(Box::new([1, 2, 3])),
            ..platform_fixture()
        });
        still_valid_config.net = Some(vec![NetConfig {
            pci_common: PciDeviceCommonConfig {
                iommu: true,
                pci_segment: 1,
                ..Default::default()
            },
            ..net_fixture()
        }]);
        still_valid_config.validate().unwrap();

        let mut still_valid_config = valid_config.clone();
        still_valid_config.platform = Some(PlatformConfig {
            iommu_segments: Some(Box::new([1, 2, 3])),
            ..platform_fixture()
        });
        still_valid_config.pmem = Some(vec![PmemConfig {
            pci_common: PciDeviceCommonConfig {
                iommu: true,
                pci_segment: 1,
                ..Default::default()
            },
            ..pmem_fixture()
        }]);
        still_valid_config.validate().unwrap();

        let mut still_valid_config = valid_config.clone();
        still_valid_config.platform = Some(PlatformConfig {
            iommu_segments: Some(Box::new([1, 2, 3])),
            ..platform_fixture()
        });
        still_valid_config.devices = Some(vec![DeviceConfig {
            pci_common: PciDeviceCommonConfig {
                iommu: true,
                pci_segment: 1,
                ..Default::default()
            },
            ..device_fixture()
        }]);
        still_valid_config.validate().unwrap();

        let mut still_valid_config = valid_config.clone();
        still_valid_config.platform = Some(PlatformConfig {
            iommu_segments: Some(Box::new([1, 2, 3])),
            ..platform_fixture()
        });
        still_valid_config.vsock = Some(VsockConfig {
            pci_common: PciDeviceCommonConfig {
                iommu: true,
                pci_segment: 1,
                ..Default::default()
            },
            cid: 3,
            socket: PathBuf::new(),
        });
        still_valid_config.validate().unwrap();

        let mut invalid_config = valid_config.clone();
        invalid_config.platform = Some(PlatformConfig {
            iommu_segments: Some(Box::new([1, 2, 3])),
            ..platform_fixture()
        });
        invalid_config.disks = Some(vec![DiskConfig {
            pci_common: PciDeviceCommonConfig {
                iommu: false,
                pci_segment: 1,
                ..Default::default()
            },
            ..disk_fixture()
        }]);
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::OnIommuSegment(1))
        );

        let mut invalid_config = valid_config.clone();
        invalid_config.platform = Some(PlatformConfig {
            iommu_segments: Some(Box::new([1, 2, 3])),
            ..platform_fixture()
        });
        invalid_config.net = Some(vec![NetConfig {
            pci_common: PciDeviceCommonConfig {
                iommu: false,
                pci_segment: 1,
                ..Default::default()
            },
            ..net_fixture()
        }]);
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::OnIommuSegment(1))
        );

        let mut invalid_config = valid_config.clone();
        invalid_config.platform = Some(PlatformConfig {
            num_pci_segments: MAX_NUM_PCI_SEGMENTS,
            iommu_segments: Some(Box::new([1, 2, 3])),
            ..platform_fixture()
        });
        invalid_config.pmem = Some(vec![PmemConfig {
            pci_common: PciDeviceCommonConfig {
                pci_segment: 1,
                ..Default::default()
            },
            ..pmem_fixture()
        }]);
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::OnIommuSegment(1))
        );

        let mut invalid_config = valid_config.clone();
        invalid_config.platform = Some(PlatformConfig {
            num_pci_segments: MAX_NUM_PCI_SEGMENTS,
            iommu_segments: Some(Box::new([1, 2, 3])),
            ..platform_fixture()
        });
        invalid_config.devices = Some(vec![DeviceConfig {
            pci_common: PciDeviceCommonConfig {
                pci_segment: 1,
                ..Default::default()
            },
            ..device_fixture()
        }]);
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::OnIommuSegment(1))
        );

        let mut invalid_config = valid_config.clone();
        invalid_config.platform = Some(PlatformConfig {
            iommu_segments: Some(Box::new([1, 2, 3])),
            ..platform_fixture()
        });
        invalid_config.vsock = Some(VsockConfig {
            pci_common: PciDeviceCommonConfig {
                pci_segment: 1,
                ..Default::default()
            },
            cid: 3,
            socket: PathBuf::new(),
        });
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::OnIommuSegment(1))
        );

        let mut invalid_config = valid_config.clone();
        invalid_config.memory.shared = true;
        invalid_config.platform = Some(PlatformConfig {
            iommu_segments: Some(Box::new([1, 2, 3])),
            ..platform_fixture()
        });
        invalid_config.user_devices = Some(vec![UserDeviceConfig {
            pci_common: PciDeviceCommonConfig {
                pci_segment: 1,
                ..Default::default()
            },
            socket: PathBuf::new(),
        }]);
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::OnIommuSegment(1))
        );

        let mut invalid_config = valid_config.clone();
        invalid_config.platform = Some(PlatformConfig {
            iommu_segments: Some(Box::new([1, 2, 3])),
            ..platform_fixture()
        });
        invalid_config.vdpa = Some(vec![VdpaConfig {
            pci_common: PciDeviceCommonConfig {
                pci_segment: 1,
                ..Default::default()
            },
            ..vdpa_fixture()
        }]);
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::OnIommuSegment(1))
        );

        let mut invalid_config = valid_config.clone();
        invalid_config.memory.shared = true;
        invalid_config.platform = Some(PlatformConfig {
            iommu_segments: Some(Box::new([1, 2, 3])),
            ..platform_fixture()
        });
        invalid_config.fs = Some(vec![FsConfig {
            pci_common: PciDeviceCommonConfig {
                pci_segment: 1,
                ..Default::default()
            },
            ..fs_fixture()
        }]);
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::OnIommuSegment(1))
        );

        let mut invalid_config = valid_config.clone();
        invalid_config.platform = Some(PlatformConfig {
            num_pci_segments: 2,
            ..platform_fixture()
        });
        invalid_config.numa = Some(Box::new([
            NumaConfig {
                guest_numa_id: 0,
                cpus: Some(Box::new([0])),
                pci_segments: Some(Box::new([1])),
                ..numa_fixture()
            },
            NumaConfig {
                guest_numa_id: 1,
                cpus: Some(Box::new([1])),
                pci_segments: Some(Box::new([1])),
                ..numa_fixture()
            },
        ]));
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::PciSegmentReused(1, 0, 1))
        );

        let mut invalid_config = valid_config.clone();
        invalid_config.pci_segments = Some(Box::new([PciSegmentConfig {
            pci_segment: 0,
            mmio32_aperture_weight: 1,
            mmio64_aperture_weight: 0,
        }]));
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::InvalidPciSegmentApertureWeight(0))
        );

        let mut invalid_config = valid_config.clone();
        invalid_config.pci_segments = Some(Box::new([PciSegmentConfig {
            pci_segment: 0,
            mmio32_aperture_weight: 0,
            mmio64_aperture_weight: 1,
        }]));
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::InvalidPciSegmentApertureWeight(0))
        );

        let mut invalid_config = valid_config.clone();
        invalid_config.numa = Some(Box::new([
            NumaConfig {
                guest_numa_id: 0,
                cpus: Some(Box::new([0])),
                ..numa_fixture()
            },
            NumaConfig {
                guest_numa_id: 1,
                cpus: Some(Box::new([1])),
                pci_segments: Some(Box::new([0])),
                ..numa_fixture()
            },
        ]));
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::DefaultPciSegmentInvalidNode(1))
        );

        let mut invalid_config = valid_config.clone();
        invalid_config.numa = Some(Box::new([
            NumaConfig {
                guest_numa_id: 0,
                cpus: Some(Box::new([0])),
                pci_segments: Some(Box::new([0])),
                ..numa_fixture()
            },
            NumaConfig {
                guest_numa_id: 1,
                cpus: Some(Box::new([1])),
                pci_segments: Some(Box::new([1])),
                ..numa_fixture()
            },
        ]));
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::InvalidPciSegment(1))
        );

        let mut invalid_config = valid_config.clone();
        invalid_config.disks = Some(vec![DiskConfig {
            rate_limit_group: Some("foo".into()),
            ..disk_fixture()
        }]);
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::InvalidRateLimiterGroup)
        );

        // Test serial length validation
        let mut valid_serial_config = valid_config.clone();
        valid_serial_config.disks = Some(vec![DiskConfig {
            serial: Some("valid_serial".to_string()),
            ..disk_fixture()
        }]);
        valid_serial_config.validate().unwrap();

        // Test empty string serial (should be valid)
        let mut empty_serial_config = valid_config.clone();
        empty_serial_config.disks = Some(vec![DiskConfig {
            serial: Some(String::new()),
            ..disk_fixture()
        }]);
        empty_serial_config.validate().unwrap();

        // Test None serial (should be valid)
        let mut none_serial_config = valid_config.clone();
        none_serial_config.disks = Some(vec![DiskConfig {
            serial: None,
            ..disk_fixture()
        }]);
        none_serial_config.validate().unwrap();

        // Test maximum length serial (exactly VIRTIO_BLK_ID_BYTES)
        let max_serial = "a".repeat(VIRTIO_BLK_ID_BYTES as usize);
        let mut max_serial_config = valid_config.clone();
        max_serial_config.disks = Some(vec![DiskConfig {
            serial: Some(max_serial),
            ..disk_fixture()
        }]);
        max_serial_config.validate().unwrap();

        // Test serial length exceeding VIRTIO_BLK_ID_BYTES
        let long_serial = "a".repeat(VIRTIO_BLK_ID_BYTES as usize + 1);
        let mut invalid_serial_config = valid_config.clone();
        invalid_serial_config.disks = Some(vec![DiskConfig {
            serial: Some(long_serial.clone()),
            ..disk_fixture()
        }]);
        assert_eq!(
            invalid_serial_config.validate(),
            Err(ValidationError::InvalidSerialLength(
                long_serial.len(),
                VIRTIO_BLK_ID_BYTES as usize
            ))
        );

        let mut still_valid_config = valid_config.clone();
        still_valid_config.devices = Some(vec![
            DeviceConfig {
                path: Some("/device1".into()),
                ..device_fixture()
            },
            DeviceConfig {
                path: Some("/device2".into()),
                ..device_fixture()
            },
        ]);
        still_valid_config.validate().unwrap();

        let mut invalid_config = valid_config.clone();
        invalid_config.devices = Some(vec![
            DeviceConfig {
                path: Some("/device1".into()),
                ..device_fixture()
            },
            DeviceConfig {
                path: Some("/device1".into()),
                ..device_fixture()
            },
        ]);
        invalid_config.validate().unwrap_err();

        // An fd-backed DeviceConfig is only valid with
        // externally-supplied fd is provided for iommufd too
        let mut fd_valid_config = valid_config.clone();
        fd_valid_config.platform = Some(PlatformConfig {
            iommufd: true,
            iommufd_fd: Some(8),
            ..platform_fixture()
        });
        fd_valid_config.devices = Some(vec![DeviceConfig {
            path: None,
            fd: Some(7),
            ..device_fixture()
        }]);
        fd_valid_config.validate().unwrap();

        let mut fd_without_iommufd_fd = fd_valid_config.clone();
        fd_without_iommufd_fd.platform = Some(PlatformConfig {
            iommufd: true,
            iommufd_fd: None,
            ..platform_fixture()
        });
        assert!(matches!(
            fd_without_iommufd_fd.validate(),
            Err(ValidationError::VfioFdRequiresIommufdFd),
        ));

        // iommufd_fd without iommufd=on is rejected at PlatformConfig::validate.
        let mut iommufd_fd_without_iommufd = valid_config.clone();
        iommufd_fd_without_iommufd.platform = Some(PlatformConfig {
            iommufd: false,
            iommufd_fd: Some(8),
            ..platform_fixture()
        });
        assert!(matches!(
            iommufd_fd_without_iommufd.validate(),
            Err(ValidationError::IommufdFdRequiresIommufd),
        ));

        // Exactly one of path and fd must be set.
        let mut both_path_and_fd = fd_valid_config.clone();
        both_path_and_fd.devices = Some(vec![DeviceConfig {
            path: Some("/device1".into()),
            fd: Some(7),
            ..device_fixture()
        }]);
        assert!(matches!(
            both_path_and_fd.validate(),
            Err(ValidationError::VfioDeviceBothPathAndFd),
        ));

        let mut neither_path_nor_fd = fd_valid_config.clone();
        neither_path_nor_fd.devices = Some(vec![DeviceConfig {
            path: None,
            fd: None,
            ..device_fixture()
        }]);
        assert!(matches!(
            neither_path_nor_fd.validate(),
            Err(ValidationError::VfioDeviceNeitherPathNorFd),
        ));
        #[cfg(feature = "sev_snp")]
        {
            // Payload with empty host data
            let mut config_with_no_host_data = valid_config.clone();
            config_with_no_host_data.payload = Some(PayloadConfig {
                kernel: Some(PathBuf::from("/path/to/kernel")),
                firmware: None,
                cmdline: None,
                initramfs: None,
                #[cfg(feature = "igvm")]
                igvm: None,
                #[cfg(feature = "sev_snp")]
                host_data: Some(String::new()),
                #[cfg(feature = "fw_cfg")]
                fw_cfg_config: None,
            });
            config_with_no_host_data.validate().unwrap_err();

            // Payload with no host data provided
            let mut valid_config_with_no_host_data = valid_config.clone();
            valid_config_with_no_host_data.payload = Some(PayloadConfig {
                kernel: Some(PathBuf::from("/path/to/kernel")),
                firmware: None,
                cmdline: None,
                initramfs: None,
                #[cfg(feature = "igvm")]
                igvm: None,
                #[cfg(feature = "sev_snp")]
                host_data: None,
                #[cfg(feature = "fw_cfg")]
                fw_cfg_config: None,
            });
            valid_config_with_no_host_data.validate().unwrap();

            // Payload with invalid host data length i.e less than 64
            let mut config_with_invalid_host_data = valid_config.clone();
            config_with_invalid_host_data.payload = Some(PayloadConfig {
                kernel: Some(PathBuf::from("/path/to/kernel")),
                firmware: None,
                cmdline: None,
                initramfs: None,
                #[cfg(feature = "igvm")]
                igvm: None,
                #[cfg(feature = "sev_snp")]
                host_data: Some(
                    "243eb7dc1a21129caa91dcbb794922b933baecb5823a377eb43118867328".to_string(),
                ),
                #[cfg(feature = "fw_cfg")]
                fw_cfg_config: None,
            });
            config_with_invalid_host_data.validate().unwrap_err();
        }

        // x_nv_gpudirect_clique with vfio_p2p_dma=off should fail
        let mut invalid_config = valid_config.clone();
        invalid_config.platform = Some(PlatformConfig {
            vfio_p2p_dma: false,
            ..platform_fixture()
        });
        invalid_config.devices = Some(vec![DeviceConfig {
            x_nv_gpudirect_clique: Some(0),
            ..device_fixture()
        }]);
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::GpuDirectCliqueRequiresP2pDma)
        );

        // x_nv_gpudirect_clique with vfio_p2p_dma=on should pass
        let mut still_valid_config = valid_config.clone();
        still_valid_config.platform = Some(PlatformConfig {
            vfio_p2p_dma: true,
            ..platform_fixture()
        });
        still_valid_config.devices = Some(vec![DeviceConfig {
            x_nv_gpudirect_clique: Some(0),
            ..device_fixture()
        }]);
        still_valid_config.validate().unwrap();

        // x_nv_gpudirect_clique with no platform config (default p2p_dma=on) should pass
        let mut still_valid_config = valid_config.clone();
        still_valid_config.devices = Some(vec![DeviceConfig {
            x_nv_gpudirect_clique: Some(0),
            ..device_fixture()
        }]);
        still_valid_config.validate().unwrap();

        // x_exclude_mmap_bars only accepts PCI BAR indices 0 through 5
        let mut invalid_config = valid_config.clone();
        invalid_config.devices = Some(vec![DeviceConfig {
            x_exclude_mmap_bars: vec![6],
            ..device_fixture()
        }]);
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::InvalidDeviceExcludeMmapBar(6))
        );

        let mut still_valid_config = valid_config.clone();
        // SAFETY: Safe as the file was just opened
        let fd1 = unsafe { libc::dup(File::open("/dev/null").unwrap().as_raw_fd()) };
        // SAFETY: Safe as the file was just opened
        let fd2 = unsafe { libc::dup(File::open("/dev/null").unwrap().as_raw_fd()) };
        // SAFETY: safe as both FDs are valid
        unsafe {
            still_valid_config.add_preserved_fds(vec![fd1, fd2]);
        }
        let _still_valid_config = still_valid_config.clone();

        // Valid BDF test
        let mut still_valid_config = valid_config.clone();
        still_valid_config.disks = Some(vec![DiskConfig {
            pci_common: PciDeviceCommonConfig {
                pci_device_id: Some(8),
                ..Default::default()
            },
            ..disk_fixture()
        }]);
        still_valid_config.validate().unwrap();
        // Invalid BDF - Same ID as Root device
        let mut invalid_config = valid_config.clone();
        invalid_config.disks = Some(vec![DiskConfig {
            pci_common: PciDeviceCommonConfig {
                pci_device_id: Some(pci::PCI_ROOT_DEVICE_ID),
                ..Default::default()
            },
            ..disk_fixture()
        }]);
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::ReservedPciDeviceId(
                pci::PCI_ROOT_DEVICE_ID
            ))
        );
        // Invalid BDF - Out of range
        let mut invalid_config = valid_config.clone();
        invalid_config.disks = Some(vec![DiskConfig {
            pci_common: PciDeviceCommonConfig {
                pci_device_id: Some(pci::NUM_DEVICE_IDS + 1),
                ..Default::default()
            },
            ..disk_fixture()
        }]);
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::InvalidPciDeviceId(pci::NUM_DEVICE_IDS + 1))
        );

        // Invalid balloon BDF - Same ID as Root device
        let mut invalid_config = valid_config.clone();
        invalid_config.balloon = Some(BalloonConfig {
            pci_common: PciDeviceCommonConfig {
                pci_device_id: Some(pci::PCI_ROOT_DEVICE_ID),
                ..Default::default()
            },
            size: 0,
            deflate_on_oom: false,
            free_page_reporting: false,
        });
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::ReservedPciDeviceId(
                pci::PCI_ROOT_DEVICE_ID
            ))
        );

        // Invalid balloon ID - Duplicate identifier
        let mut invalid_config = valid_config.clone();
        invalid_config.balloon = Some(BalloonConfig {
            pci_common: PciDeviceCommonConfig {
                id: Some("test0".to_string()),
                ..Default::default()
            },
            size: 0,
            deflate_on_oom: false,
            free_page_reporting: false,
        });
        invalid_config.disks = Some(vec![DiskConfig {
            pci_common: PciDeviceCommonConfig {
                id: Some("test0".to_string()),
                ..Default::default()
            },
            ..disk_fixture()
        }]);
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::IdentifierNotUnique("test0".to_string()))
        );

        // Invalid console BDF - Same ID as Root device
        let mut invalid_config = valid_config.clone();
        invalid_config.console.pci_common.pci_device_id = Some(pci::PCI_ROOT_DEVICE_ID);
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::ReservedPciDeviceId(
                pci::PCI_ROOT_DEVICE_ID
            ))
        );
        // Invalid console BDF - Out of range
        let mut invalid_config = valid_config.clone();
        invalid_config.console.pci_common.pci_device_id = Some(pci::NUM_DEVICE_IDS + 1);
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::InvalidPciDeviceId(pci::NUM_DEVICE_IDS + 1))
        );
        // Invalid console ID - Duplicate identifier
        let mut invalid_config = valid_config.clone();
        invalid_config.console.pci_common.id = Some("test0".to_string());
        invalid_config.disks = Some(vec![DiskConfig {
            pci_common: PciDeviceCommonConfig {
                id: Some("test0".to_string()),
                ..Default::default()
            },
            ..disk_fixture()
        }]);
        assert_eq!(
            invalid_config.validate(),
            Err(ValidationError::IdentifierNotUnique("test0".to_string()))
        );
    }
    #[test]
    fn test_landlock_parsing() -> Result<()> {
        // should not be empty
        LandlockConfig::parse("").unwrap_err();
        // access should not be empty
        LandlockConfig::parse("path=/dir/path1").unwrap_err();
        LandlockConfig::parse("path=/dir/path1,access=rwr").unwrap_err();
        assert_eq!(
            LandlockConfig::parse("path=/dir/path1,access=rw")?,
            LandlockConfig {
                path: PathBuf::from("/dir/path1"),
                access: "rw".to_string(),
            }
        );
        Ok(())
    }
    #[test]
    #[cfg(feature = "fw_cfg")]
    fn test_fw_cfg_config_item_list_parsing() -> Result<()> {
        // Empty list
        FwCfgConfig::parse("items=[]").unwrap_err();
        // Missing closing bracket
        FwCfgConfig::parse("items=[name=opt/org.test/fw_cfg_test_item,file=/tmp/fw_cfg_test_item")
            .unwrap_err();
        // Single file Item
        assert_eq!(
            FwCfgConfig::parse(
                "items=[name=opt/org.test/fw_cfg_test_item,file=/tmp/fw_cfg_test_item]"
            )?,
            FwCfgConfig {
                items: Some(FwCfgItemList {
                    item_list: vec![FwCfgItem {
                        name: "opt/org.test/fw_cfg_test_item".to_string(),
                        file: Some(PathBuf::from("/tmp/fw_cfg_test_item")),
                        string: None,
                    }]
                }),
                ..Default::default()
            },
        );
        // Multiple file Items
        assert_eq!(
            FwCfgConfig::parse(
                "items=[name=opt/org.test/fw_cfg_test_item,file=/tmp/fw_cfg_test_item:name=opt/org.test/fw_cfg_test_item2,file=/tmp/fw_cfg_test_item2]"
            )?,
            FwCfgConfig {
                items: Some(FwCfgItemList {
                    item_list: vec![
                        FwCfgItem {
                            name: "opt/org.test/fw_cfg_test_item".to_string(),
                            file: Some(PathBuf::from("/tmp/fw_cfg_test_item")),
                            string: None,
                        },
                        FwCfgItem {
                            name: "opt/org.test/fw_cfg_test_item2".to_string(),
                            file: Some(PathBuf::from("/tmp/fw_cfg_test_item2")),
                            string: None,
                        }
                    ]
                }),
                ..Default::default()
            },
        );
        // Single string Item (for OVMF MMIO64 config, GPU CC passthrough, etc.)
        assert_eq!(
            FwCfgConfig::parse("items=[name=opt/ovmf/X-PciMmio64Mb,string=262144]")?,
            FwCfgConfig {
                items: Some(FwCfgItemList {
                    item_list: vec![FwCfgItem {
                        name: "opt/ovmf/X-PciMmio64Mb".to_string(),
                        file: None,
                        string: Some("262144".to_string()),
                    }]
                }),
                ..Default::default()
            },
        );
        // Mixed file and string Items
        assert_eq!(
            FwCfgConfig::parse(
                "items=[name=opt/org.test/fw_cfg_test_item,file=/tmp/fw_cfg_test_item:name=opt/ovmf/X-PciMmio64Mb,string=262144]"
            )?,
            FwCfgConfig {
                items: Some(FwCfgItemList {
                    item_list: vec![
                        FwCfgItem {
                            name: "opt/org.test/fw_cfg_test_item".to_string(),
                            file: Some(PathBuf::from("/tmp/fw_cfg_test_item")),
                            string: None,
                        },
                        FwCfgItem {
                            name: "opt/ovmf/X-PciMmio64Mb".to_string(),
                            file: None,
                            string: Some("262144".to_string()),
                        }
                    ]
                }),
                ..Default::default()
            },
        );
        // Missing both file and string parses OK but fails validation
        let missing_content =
            FwCfgConfig::parse("items=[name=opt/org.test/missing_content]").unwrap();
        assert_eq!(
            missing_content.items.as_ref().unwrap().item_list[0].file,
            None
        );
        assert_eq!(
            missing_content.items.as_ref().unwrap().item_list[0].string,
            None
        );
        // Both file and string parses OK but fails validation
        let both = FwCfgConfig::parse("items=[name=opt/org.test/both,file=/tmp/test,string=test]")
            .unwrap();
        assert!(both.items.as_ref().unwrap().item_list[0].file.is_some());
        assert!(both.items.as_ref().unwrap().item_list[0].string.is_some());
        Ok(())
    }
}
