Files
punktfunk/packaging/windows/drivers/vendor/wdk-build/src/lib.rs
T
enricobuehler 9fd19b90a9
windows-drivers / probe-and-proto (push) Successful in 24s
apple / swift (push) Successful in 1m8s
windows-drivers / driver-build (push) Failing after 43s
ci / rust (push) Successful in 1m31s
ci / web (push) Successful in 1m5s
ci / docs-site (push) Successful in 52s
apple / screenshots (push) Failing after 2m35s
windows-host / package (push) Successful in 5m23s
ci / bench (push) Successful in 4m48s
android / android (push) Successful in 10m1s
decky / build-publish (push) Successful in 26s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 4s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
deb / build-publish (push) Successful in 3m29s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 2m21s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 8m23s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 8m18s
docker / deploy-docs (push) Successful in 21s
feat(windows-drivers): vendor wdk 0.5.1 + add ApiSubset::Iddcx (M1 spike)
Vendor the published, self-contained windows-drivers-rs 0.5.1 crates
(wdk-build, wdk-sys) under vendor/ and add a first-class ApiSubset::Iddcx that
bindgens iddcx/1.10/IddCx.h in an extra pass reusing bindgen::Builder::wdk_default
(allowlist_file (?i).*iddcx.* — emits only IddCx items; WDF/DXGI types resolve to
the shared base/wdf bindings, type-identity by construction). Mirrors the existing
gpio/hid/spb subsets exactly: wdk-build gets the enum variant + iddcx_headers()
(UMDF-only), wdk-sys gets generate_iddcx + the iddcx feature + pub mod iddcx.
[patch.crates-io] redirects all wdk-sys/wdk-build (incl. wdk 0.4.1 transitive) to
the patched copies. wdk-probe enables the iddcx feature.

MAKE-OR-BREAK: does IddCx.h bindgen in wdk-sys config without a header conflict
(issue #515) + does the generated module compile (type-identity)? CI answers it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:12:43 +00:00

2233 lines
82 KiB
Rust

// Copyright (c) Microsoft Corporation
// License: MIT OR Apache-2.0
//! The [`wdk-build`][crate] crate is a library that is used within Cargo build
//! scripts to configure any build that depends on the WDK (Windows Driver Kit).
//! This is especially useful for crates that generate FFI bindings to the WDK,
//! WDK-dependent libraries, and programs built on top of the WDK (ex. Drivers).
//! This library is built to be able to accommodate different WDK releases, as
//! well strives to allow for all the configuration the WDK allows. This
//! includes being ables to select different WDF versions and different driver
//! models (WDM, KMDF, UMDF).
#![cfg_attr(nightly_toolchain, feature(assert_matches))]
use std::{
env,
fmt,
path::{Path, PathBuf, absolute},
str::FromStr,
sync::LazyLock,
};
pub use bindgen::BuilderExt;
use metadata::TryFromCargoMetadataError;
use tracing::debug;
pub mod cargo_make;
pub mod metadata;
mod utils;
mod bindgen;
use cargo_metadata::MetadataCommand;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::utils::detect_windows_sdk_version;
/// Configuration parameters for a build dependent on the WDK
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Config {
/// Path to root of WDK. Corresponds with `WDKContentRoot` environment
/// variable in eWDK
wdk_content_root: PathBuf,
/// CPU architecture to target
cpu_architecture: CpuArchitecture,
/// Build configuration of driver
pub driver_config: DriverConfig,
}
/// The driver type with its associated configuration parameters
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(
tag = "DRIVER_TYPE",
deny_unknown_fields,
rename_all = "UPPERCASE",
from = "DeserializableDriverConfig"
)]
pub enum DriverConfig {
/// Windows Driver Model
Wdm,
/// Kernel Mode Driver Framework
Kmdf(KmdfConfig),
/// User Mode Driver Framework
Umdf(UmdfConfig),
}
/// Private enum identical to [`DriverConfig`] but with different tag name to
/// deserialize from.
///
/// [`serde_derive`] doesn't support different tag names for serialization vs.
/// deserialization, and also doesn't support aliases for tag names, so the
/// `from` attribute is used in conjunction with this type to facilitate a
/// different tag name for deserialization.
///
/// Relevant Github Issues:
/// * <https://github.com/serde-rs/serde/issues/2776>
/// * <https://github.com/serde-rs/serde/issues/2324>
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Hash)]
#[serde(tag = "driver-type", deny_unknown_fields, rename_all = "UPPERCASE")]
enum DeserializableDriverConfig {
Wdm,
Kmdf(KmdfConfig),
Umdf(UmdfConfig),
}
/// The CPU architecture that's configured to be compiled for
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum CpuArchitecture {
/// AMD64 CPU architecture. Also known as x64 or x86-64.
Amd64,
/// ARM64 CPU architecture. Also known as aarch64.
Arm64,
}
impl FromStr for CpuArchitecture {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"amd64" => Ok(Self::Amd64),
"arm64" => Ok(Self::Arm64),
_ => Err(format!("'{s}' is not a valid target architecture")),
}
}
}
impl fmt::Display for CpuArchitecture {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Self::Amd64 => "amd64",
Self::Arm64 => "arm64",
};
write!(f, "{s}")
}
}
/// The configuration parameters for KMDF drivers
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(
deny_unknown_fields,
rename_all(serialize = "SCREAMING_SNAKE_CASE", deserialize = "kebab-case")
)]
pub struct KmdfConfig {
/// Major KMDF Version
pub kmdf_version_major: u8,
/// Minor KMDF Version (Target Version)
pub target_kmdf_version_minor: u8,
/// Minor KMDF Version (Minimum Required)
pub minimum_kmdf_version_minor: Option<u8>,
}
/// The configuration parameters for UMDF drivers
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(
deny_unknown_fields,
rename_all(serialize = "SCREAMING_SNAKE_CASE", deserialize = "kebab-case")
)]
pub struct UmdfConfig {
/// Major UMDF Version
pub umdf_version_major: u8,
/// Minor UMDF Version (Target Version)
pub target_umdf_version_minor: u8,
/// Minor UMDF Version (Minimum Required)
pub minimum_umdf_version_minor: Option<u8>,
}
/// Metadata providing additional context for [`std::io::Error`] failures
///
/// This enum provides structured information about the file system paths
/// or operations that led to an I/O error. It can represent either single
/// path operations or operations involving both source and destination paths.
#[non_exhaustive]
#[derive(Debug, Error)]
pub enum IoErrorMetadata {
/// Path related to [`std::io::Error`] failure
#[error(r#"failed to perform an IO operation on "{path}""#)]
Path {
/// The file system path where the I/O error occurred
path: PathBuf,
},
/// Source and destination paths related to [`std::io::Error`] failure.
///
/// This can be provided for APIs like [`std::fs::copy`] which have both a
/// `from` and `to` path.
#[error(r#"failed to perform an IO operation from "{from_path}" to "{to_path}""#)]
SrcDestPaths {
/// The source path in a copy or move operation that failed
from_path: PathBuf,
/// The destination path in a copy or move operation that failed
to_path: PathBuf,
},
}
/// Dedicated error type for I/O operations with extra metadata context
///
/// This error type wraps [`std::io::Error`] with additional [`IoErrorMetadata`]
/// to provide better context about which file system paths or operations
/// failed. It can be used directly by functions that only perform I/O
/// operations, and automatically converts to [`ConfigError`] when needed.
#[derive(Debug, Error)]
#[error("{metadata}")]
pub struct IoError {
/// Extra metadata related to the error
metadata: IoErrorMetadata,
/// [`std::io::Error`] that caused the operation to fail
#[source]
source: std::io::Error,
}
impl IoError {
/// Creates a new `IoError` with a single path and source error.
pub fn with_path(path: impl Into<PathBuf>, source: std::io::Error) -> Self {
Self {
metadata: IoErrorMetadata::Path { path: path.into() },
source,
}
}
/// Creates a new `IoError` for operations involving a source and
/// destination path.
pub fn with_src_dest_paths(
from_path: impl Into<PathBuf>,
to_path: impl Into<PathBuf>,
source: std::io::Error,
) -> Self {
Self {
metadata: IoErrorMetadata::SrcDestPaths {
from_path: from_path.into(),
to_path: to_path.into(),
},
source,
}
}
}
/// Errors that could result from configuring a build via [`wdk_build`][crate]
#[derive(Debug, Error)]
pub enum ConfigError {
/// Error returned when an [`std::io`] operation fails
#[error(transparent)]
IoError(#[from] IoError),
/// Error returned when an expected directory does not exist
#[error("cannot find directory: {directory}")]
DirectoryNotFound {
/// Path of directory that was not found
directory: String,
},
/// Error returned when a package is not found in Cargo metadata
#[error("cannot find wdk-build package in Cargo metadata")]
WdkBuildPackageNotFoundInCargoMetadata,
/// Error returned Cargo manifest contains an unsupported edition
#[error("Cargo manifest contains unsupported Rust edition: {edition}")]
UnsupportedRustEdition {
/// Edition of the Cargo manifest that was not supported
edition: String,
},
/// Error returned when `bindgen` does not support `rust-version` in Cargo
/// manifest
#[error("Rust version {msrv} not supported by Bindgen: {reason}")]
MsrvNotSupportedByBindgen {
/// MSRV that was not supported by Bindgen
msrv: String,
/// Reason why the MSRV was not supported
reason: String,
},
/// Error returned when `semver` parsing of the Rust version fails
#[error("failed to parse rust-version in manifest")]
RustVersionParseError {
/// [`semver::Error`] that caused parsing the Rust version to fail
#[source]
error_source: semver::Error,
},
/// Error returned when a [`metadata::Wdk`] fails to be parsed from a Cargo
/// Manifest
#[error(transparent)]
TryFromCargoMetadataError(#[from] metadata::TryFromCargoMetadataError),
/// Error returned when a [`Config`] fails to be serialized
#[error(
"WDKContentRoot should be able to be detected. Ensure that the WDK is installed, or that \
the environment setup scripts in the eWDK have been run."
)]
WdkContentRootDetectionError,
/// Error returned when the WDK version string does not match the expected
/// format
#[error("the WDK version string provided ({version}) was not in a valid format")]
WdkVersionStringFormatError {
/// The incorrect WDK version string.
version: String,
},
/// Error returned when `cargo_metadata` execution or parsing fails
#[error(transparent)]
CargoMetadataError(#[from] cargo_metadata::Error),
/// Error returned when no wdk-build package is detected
#[error("no wdk-build package is detected")]
NoWdkBuildCrateDetected,
/// Error returned when multiple versions of the wdk-build package are
/// detected
#[error(
"multiple versions of the wdk-build package are detected, but only one version is \
allowed: {package_ids:#?}"
)]
MultipleWdkBuildCratesDetected {
/// package ids of the wdk-build crates detected
package_ids: Vec<cargo_metadata::PackageId>,
},
/// Error returned when the c runtime is not configured to be statically
/// linked
#[error(
"the C runtime is not properly configured to be statically linked. This is required for building WDK drivers. The recommended solution is to add the following snippet to a \
`.cargo/config.toml` file:
[build]
rustflags = [\"-C\", \"target-feature=+crt-static\"]
\
See https://doc.rust-lang.org/reference/linkage.html#static-and-dynamic-c-runtimes for more ways \
to enable static crt linkage"
)]
StaticCrtNotEnabled,
/// Error returned when [`metadata::ser::Serializer`] fails to serialize the
/// [`metadata::Wdk`]
#[error(transparent)]
SerdeError(#[from] metadata::Error),
}
/// Subset of APIs in the Windows Driver Kit
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum ApiSubset {
/// API subset typically required for all Windows drivers
Base,
/// API subset required for WDF (Windows Driver Framework) drivers: <https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/_wdf/>
Wdf,
/// API subset for GPIO (General Purpose Input/Output) drivers: <https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/_gpio/>
Gpio,
/// API subset for HID (Human Interface Device) drivers: <https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/_hid/>
Hid,
/// API subset for Parallel Ports drivers: <https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/_parports/>
ParallelPorts,
/// API subset for SPB (Serial Peripheral Bus) drivers: <https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/_spb/>
Spb,
/// API subset for Storage drivers: <https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/_storage/>
Storage,
/// API subset for USB (Universal Serial Bus) drivers: <https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/_usbref/>
Usb,
/// API subset for IddCx (Indirect Display, UMDF-only) drivers: <https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/iddcx/>
Iddcx,
}
#[derive(Debug, Error, PartialEq, Eq)]
/// Error when parsing a [`TwoPartVersion`].
pub enum TwoPartVersionError {
/// Supplied string didn't match MAJOR.MINOR format.
#[error("Invalid version: {0}. Expected format is 'major.minor'")]
InvalidFormat(String),
/// A numeric component failed to parse (component name, original string).
#[error("Error parsing {0} version to 'u32'. Version string: {1}")]
ParseError(String, String),
}
/// Version of the form MAJOR.MINOR (both u32). Accepts leading zeros.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
pub struct TwoPartVersion(pub u32, pub u32);
/// Parses a string of the form `MAJOR.MINOR` into a [`TwoPartVersion`].
///
/// # Expected format
/// - The input string must contain exactly one dot (`.`) separating two
/// non-empty components.
/// - Both components must be valid unsigned 32-bit integers (`u32`). Leading
/// zeros are accepted.
///
/// # Errors
/// - Returns [`TwoPartVersionError::InvalidFormat`] if the string does not
/// contain exactly one dot or has empty components.
/// - Returns [`TwoPartVersionError::ParseError`] if either component cannot be
/// parsed as a `u32`.
impl FromStr for TwoPartVersion {
type Err = TwoPartVersionError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let dot_count = s.matches('.').count();
if dot_count != 1 {
return Err(TwoPartVersionError::InvalidFormat(s.to_string()));
}
let (major_str, minor_str) = s
.split_once('.')
.ok_or_else(|| TwoPartVersionError::InvalidFormat(s.to_string()))?;
if major_str.is_empty() || minor_str.is_empty() {
return Err(TwoPartVersionError::InvalidFormat(s.to_string()));
}
let major = major_str
.parse::<u32>()
.map_err(|_| TwoPartVersionError::ParseError("major".to_string(), s.to_string()))?;
let minor = minor_str
.parse::<u32>()
.map_err(|_| TwoPartVersionError::ParseError("minor".to_string(), s.to_string()))?;
Ok(Self(major, minor))
}
}
impl Default for Config {
fn default() -> Self {
Self {
wdk_content_root: utils::detect_wdk_content_root().expect(
"WDKContentRoot should be able to be detected. Ensure that the WDK is installed, \
or that the environment setup scripts in the eWDK have been run.",
),
driver_config: DriverConfig::Wdm,
cpu_architecture: utils::detect_cpu_architecture_in_build_script(),
}
}
}
impl Config {
/// Create a new [`Config`] with default values
#[must_use]
#[tracing::instrument(level = "debug")]
pub fn new() -> Self {
Self::default()
}
/// Create a [`Config`] from parsing the top-level Cargo manifest into a
/// [`metadata::Wdk`], and using it to populate the [`Config`]. It also
/// emits `cargo::rerun-if-changed` directives for any files that are
/// used to create the [`Config`].
///
/// # Errors
///
/// This function will return an error if:
/// * the execution of `cargo metadata` fails
/// * the parsing of [`metadata::Wdk`] from any of the Cargo manifests fail
/// * multiple conflicting [`metadata::Wdk`] configurations are detected
/// * no [`metadata::Wdk`] configurations are detected
///
/// # Panics
///
/// Panics if the resolved top-level Cargo manifest path is not valid UTF-8
#[tracing::instrument(level = "debug")]
pub fn from_env_auto() -> Result<Self, ConfigError> {
let top_level_cargo_manifest_path = find_top_level_cargo_manifest();
debug!(
"Top level Cargo manifest path: {:?}",
top_level_cargo_manifest_path
);
let cwd = top_level_cargo_manifest_path
.parent()
.expect("Cargo manifest should have a valid parent directory");
let cargo_metadata = MetadataCommand::new()
// Run `cargo_metadata` in the same working directory as the top level manifest in order
// to respect `config.toml` overrides
.current_dir(cwd)
// top-level manifest path must be used in order for metadata from the top-level crates
// to be discovered
.manifest_path(&top_level_cargo_manifest_path)
.exec()?;
let wdk_metadata = metadata::Wdk::try_from(&cargo_metadata)?;
// Force rebuilds if any of the manifest files change (ex. if wdk metadata
// section is modified)
for manifest_path in metadata::iter_manifest_paths(cargo_metadata)
.into_iter()
.chain(std::iter::once(
top_level_cargo_manifest_path
.try_into()
.expect("Path to Cargo manifests should always be valid UTF8"),
))
{
println!("cargo:rerun-if-changed={manifest_path}");
}
Ok(Self {
driver_config: wdk_metadata.driver_model,
..Default::default()
})
}
/// Emit `cargo::rustc-check-cfg` directives corresponding to all the
/// possible `rustc-cfg` settings `wdk_build` could emit
///
/// This is useful in situations where a library may not have a valid WDK
/// config available during build. This function is not needed if the build
/// was already configured via [`configure_wdk_binary_build`],
/// [`configure_wdk_library_build`], or
/// [`configure_wdk_library_build_and_then`]
#[tracing::instrument(level = "debug")]
pub fn emit_check_cfg_settings() {
for (cfg_key, allowed_values) in EXPORTED_CFG_SETTINGS.iter() {
let allowed_cfg_value_string =
allowed_values.iter().fold(String::new(), |mut acc, value| {
const OPENING_QUOTE: char = '"';
const CLOSING_QUOTE_AND_COMMA: &str = r#"","#;
acc.reserve(
value.len() + OPENING_QUOTE.len_utf8() + CLOSING_QUOTE_AND_COMMA.len(),
);
acc.push(OPENING_QUOTE);
acc.push_str(value);
acc.push_str(CLOSING_QUOTE_AND_COMMA);
acc
});
let cfg_key = {
// Replace `metadata::ser::KEY_NAME_SEPARATOR` with `__` so that `cfg_key` is a
// valid rust identifier name
let mut k = cfg_key.replace(metadata::ser::KEY_NAME_SEPARATOR, "__");
// convention is that cfg keys are lowercase
k.make_ascii_lowercase();
k
};
// Emit allowed cfg values
println!("cargo::rustc-check-cfg=cfg({cfg_key}, values({allowed_cfg_value_string}))");
}
}
/// Expose `cfg` settings based on this [`Config`] to enable conditional
/// compilation. This emits specially formatted prints to Cargo based on
/// this [`Config`].
#[tracing::instrument(level = "trace")]
fn emit_cfg_settings(&self) -> Result<(), ConfigError> {
Self::emit_check_cfg_settings();
let serialized_wdk_metadata_map =
metadata::to_map::<std::collections::BTreeMap<_, _>>(&metadata::Wdk {
driver_model: self.driver_config.clone(),
})?;
for cfg_key in EXPORTED_CFG_SETTINGS.iter().map(|(key, _)| *key) {
let cfg_value = &serialized_wdk_metadata_map[cfg_key];
let cfg_key = {
// Replace `metadata::ser::KEY_NAME_SEPARATOR` with `__` so that `cfg_key` is a
// valid rust identifier name
let mut k = cfg_key.replace(metadata::ser::KEY_NAME_SEPARATOR, "__");
// convention is that cfg keys are lowercase
k.make_ascii_lowercase();
k
};
// Emit cfg
println!(r#"cargo::rustc-cfg={cfg_key}="{cfg_value}""#);
}
Ok(())
}
/// Return header include paths required to build and link based off of the
/// configuration of `Config`
///
/// # Errors
///
/// This function will return an error if any of the required paths do not
/// exist.
#[tracing::instrument(level = "debug")]
pub fn include_paths(&self) -> Result<impl Iterator<Item = PathBuf>, ConfigError> {
let mut include_paths = vec![];
let sdk_version = detect_windows_sdk_version(&self.wdk_content_root)?;
let include_directory = self.wdk_content_root.join("Include");
// Add windows sdk include paths
// Based off of logic from WindowsDriver.KernelMode.props &
// WindowsDriver.UserMode.props in NI(22H2) WDK
let windows_sdk_include_path = include_directory.join(sdk_version);
let crt_include_path = windows_sdk_include_path.join("km/crt");
Self::validate_and_add_folder_path(&mut include_paths, &crt_include_path)?;
let km_or_um_include_path = windows_sdk_include_path.join(match self.driver_config {
DriverConfig::Wdm | DriverConfig::Kmdf(_) => "km",
DriverConfig::Umdf(_) => "um",
});
Self::validate_and_add_folder_path(&mut include_paths, &km_or_um_include_path)?;
let kit_shared_include_path = windows_sdk_include_path.join("shared");
Self::validate_and_add_folder_path(&mut include_paths, &kit_shared_include_path)?;
// Add other driver type-specific include paths
match &self.driver_config {
DriverConfig::Wdm => {}
DriverConfig::Kmdf(kmdf_config) => {
let kmdf_include_path = include_directory.join(format!(
"wdf/kmdf/{}.{}",
kmdf_config.kmdf_version_major, kmdf_config.target_kmdf_version_minor
));
Self::validate_and_add_folder_path(&mut include_paths, &kmdf_include_path)?;
// `ufxclient.h` relies on `ufxbase.h` being on the headers search path. The WDK
// normally does not automatically include this search path, but it is required
// here so that the headers can be processed successfully.
let ufx_include_path = km_or_um_include_path.join("ufx/1.1");
Self::validate_and_add_folder_path(&mut include_paths, &ufx_include_path)?;
}
DriverConfig::Umdf(umdf_config) => {
let umdf_include_path = include_directory.join(format!(
"wdf/umdf/{}.{}",
umdf_config.umdf_version_major, umdf_config.target_umdf_version_minor
));
Self::validate_and_add_folder_path(&mut include_paths, &umdf_include_path)?;
}
}
Ok(include_paths.into_iter())
}
/// Validate that a path refers to an existing directory and push its
/// canonical absolute form into the provided collection.
///
/// This helper is used for both header include directories and library
/// directories. It normalizes paths before insertion.
fn validate_and_add_folder_path(
include_paths: &mut Vec<PathBuf>,
path: &Path,
) -> Result<(), ConfigError> {
// Include paths should be directories
if !path.is_dir() {
return Err(ConfigError::DirectoryNotFound {
directory: path.to_string_lossy().into(),
});
}
let absolute_path = absolute(path).map_err(|source| IoError::with_path(path, source))?;
include_paths.push(absolute_path);
Ok(())
}
/// Return library include paths required to build and link based off of
/// the configuration of [`Config`].
///
/// For UMDF drivers, this assumes a "Windows-Driver" Target Platform.
///
/// # Errors
///
/// This function will return an error if any of the required paths do not
/// exist.
#[tracing::instrument(level = "debug")]
pub fn library_paths(&self) -> Result<impl Iterator<Item = PathBuf>, ConfigError> {
let mut library_paths = vec![];
let sdk_version = detect_windows_sdk_version(&self.wdk_content_root)?;
// Add windows sdk library paths
// Based off of logic from WindowsDriver.KernelMode.props &
// WindowsDriver.UserMode.props in NI(22H2) WDK
let windows_sdk_library_path = self.sdk_library_path(sdk_version)?;
Self::validate_and_add_folder_path(&mut library_paths, &windows_sdk_library_path)?;
// Add other driver type-specific library paths
let library_directory = self.wdk_content_root.join("Lib");
match &self.driver_config {
DriverConfig::Wdm => (),
DriverConfig::Kmdf(kmdf_config) => {
let kmdf_library_path = library_directory.join(format!(
"wdf/kmdf/{}/{}.{}",
self.cpu_architecture.as_windows_str(),
kmdf_config.kmdf_version_major,
kmdf_config.target_kmdf_version_minor
));
Self::validate_and_add_folder_path(&mut library_paths, &kmdf_library_path)?;
}
DriverConfig::Umdf(umdf_config) => {
let umdf_library_path = library_directory.join(format!(
"wdf/umdf/{}/{}.{}",
self.cpu_architecture.as_windows_str(),
umdf_config.umdf_version_major,
umdf_config.target_umdf_version_minor,
));
Self::validate_and_add_folder_path(&mut library_paths, &umdf_library_path)?;
}
}
// Reverse order of library paths so that paths pushed later into the vec take
// precedence
library_paths.reverse();
Ok(library_paths.into_iter())
}
/// Return an iterator of strings that represent compiler definitions
/// derived from the `Config`
#[tracing::instrument(level = "debug")]
pub fn preprocessor_definitions(&self) -> impl Iterator<Item = (String, Option<String>)> {
// _WIN32_WINNT=$(WIN32_WINNT_VERSION);
// WINVER=$(WINVER_VERSION);
// WINNT=1;
// NTDDI_VERSION=$(NTDDI_VERSION);
// Definition sourced from: Program Files\Windows
// Kits\10\build\10.0.26040.0\WindowsDriver.Shared.Props
// vec![ //from driver.os.props //D:\EWDK\rsprerelease\content\Program
// Files\Windows Kits\10\build\10.0.26040.0\WindowsDriver.OS.Props
// ("_WIN32_WINNT", Some()),CURRENT_WIN32_WINNT_VERSION
// ("WINVER", Some()), = CURRENT_WIN32_WINNT_VERSION
// ("WINNT", Some(1)),1
// ("NTDDI_VERSION", Some()),CURRENT_NTDDI_VERSION
// ]
// .into_iter()
// .map(|(key, value)| (key.to_string(), value.map(|v| v.to_string())))
match self.cpu_architecture {
// Definitions sourced from `Program Files\Windows
// Kits\10\build\10.0.22621.0\WindowsDriver.x64.props`
CpuArchitecture::Amd64 => {
vec![("_WIN64", None), ("_AMD64_", None), ("AMD64", None)]
}
// Definitions sourced from `Program Files\Windows
// Kits\10\build\10.0.22621.0\WindowsDriver.arm64.props`
CpuArchitecture::Arm64 => {
vec![
("_ARM64_", None),
("ARM64", None),
("_USE_DECLSPECS_FOR_SAL", Some(1)),
("STD_CALL", None),
]
}
}
.into_iter()
.map(|(key, value)| (key.to_string(), value.map(|v| v.to_string())))
.chain(
match self.driver_config {
DriverConfig::Wdm => {
vec![
("_KERNEL_MODE", None), // Normally defined by msvc via /kernel flag
]
}
DriverConfig::Kmdf(kmdf_config) => {
let mut kmdf_definitions = vec![
("_KERNEL_MODE", None), // Normally defined by msvc via /kernel flag
("KMDF_VERSION_MAJOR", Some(kmdf_config.kmdf_version_major)),
(
"KMDF_VERSION_MINOR",
Some(kmdf_config.target_kmdf_version_minor),
),
];
if let Some(minimum_minor_version) = kmdf_config.minimum_kmdf_version_minor {
kmdf_definitions
.push(("KMDF_MINIMUM_VERSION_REQUIRED", Some(minimum_minor_version)));
}
kmdf_definitions
}
DriverConfig::Umdf(umdf_config) => {
let mut umdf_definitions = vec![
("UMDF_VERSION_MAJOR", Some(umdf_config.umdf_version_major)),
(
"UMDF_VERSION_MINOR",
Some(umdf_config.target_umdf_version_minor),
),
// Definition sourced from: Program Files\Windows
// Kits\10\build\10.0.26040.0\Windows.UserMode.props
("_ATL_NO_WIN_SUPPORT", None),
// Definition sourced from: Program Files\Windows
// Kits\10\build\10.0.26040.0\WindowsDriver.Shared.Props
("WIN32_LEAN_AND_MEAN", Some(1)),
];
if let Some(minimum_minor_version) = umdf_config.minimum_umdf_version_minor {
umdf_definitions
.push(("UMDF_MINIMUM_VERSION_REQUIRED", Some(minimum_minor_version)));
}
if umdf_config.umdf_version_major >= 2 {
umdf_definitions.push(("UMDF_USING_NTSTATUS", None));
umdf_definitions.push(("_UNICODE", None));
umdf_definitions.push(("UNICODE", None));
}
umdf_definitions
}
}
.into_iter()
.map(|(key, value)| (key.to_string(), value.map(|v| v.to_string()))),
)
}
/// Return an iterator of strings that represent compiler flags (i.e.
/// warnings, settings, etc.) used by bindgen to parse WDK headers
#[tracing::instrument(level = "debug")]
pub fn wdk_bindgen_compiler_flags() -> impl Iterator<Item = String> {
vec![
// Enable Microsoft C/C++ extensions and compatibility options (https://clang.llvm.org/docs/UsersManual.html#microsoft-extensions)
"-fms-compatibility",
"-fms-extensions",
"-fdelayed-template-parsing",
// Windows SDK & DDK have non-portable paths (ex. #include "DriverSpecs.h" but the
// file is actually driverspecs.h)
"--warn-=no-nonportable-include-path",
// Windows SDK & DDK use pshpack and poppack headers to change packing
"--warn-=no-pragma-pack",
"--warn-=no-ignored-attributes",
"--warn-=no-ignored-pragma-intrinsic",
"--warn-=no-visibility",
"--warn-=no-switch",
"--warn-=no-comment",
// Don't warn for deprecated declarations. Deprecated items should be explicitly
// blocklisted (i.e. by the bindgen invocation). Any non-blocklisted function
// definitions will trigger a -WDeprecated warning
"--warn-=no-deprecated-declarations",
// Windows SDK & DDK contain unnecessary token pasting (ex. &##_variable: `&` and
// `_variable` are separate tokens already, and don't need `##` to concatenate
// them)
"--warn-=no-invalid-token-paste",
// Windows SDK & DDK headers rely on Microsoft extensions to C/C++
"--warn-=no-microsoft",
]
.into_iter()
.map(ToString::to_string)
}
/// Returns a [`String`] iterator over all the headers for a given
/// [`ApiSubset`]
///
/// The iterator considers both the [`ApiSubset`] and the [`Config`] to
/// determine which headers to yield
///
/// # Errors
/// [`ConfigError`] - if the headers for the given [`ApiSubset`] could not
/// be determined
#[tracing::instrument(level = "debug")]
pub fn headers(
&self,
api_subset: ApiSubset,
) -> Result<impl Iterator<Item = String>, ConfigError> {
let headers = match api_subset {
ApiSubset::Base => self.base_headers(),
ApiSubset::Wdf => self.wdf_headers(),
ApiSubset::Gpio => self.gpio_headers(),
ApiSubset::Hid => self.hid_headers(),
ApiSubset::ParallelPorts => self.parallel_ports_headers(),
ApiSubset::Spb => self.spb_headers(),
ApiSubset::Storage => self.storage_headers(),
ApiSubset::Usb => return self.usb_headers().map(std::iter::IntoIterator::into_iter),
ApiSubset::Iddcx => self.iddcx_headers(),
};
Ok(headers
.into_iter()
.map(String::from)
.collect::<Vec<_>>()
.into_iter())
}
#[tracing::instrument(level = "trace")]
fn iddcx_headers(&self) -> Vec<&'static str> {
// IddCx (Indirect Display) is UMDF-only. Versioned subpath like spb/1.1 and HidSpiCx/1.0.
if matches!(self.driver_config, DriverConfig::Umdf(_)) {
vec!["iddcx/1.10/IddCx.h"]
} else {
vec![]
}
}
#[tracing::instrument(level = "trace")]
fn base_headers(&self) -> Vec<&'static str> {
match &self.driver_config {
DriverConfig::Wdm | DriverConfig::Kmdf(_) => {
vec!["ntifs.h", "ntddk.h", "ntstrsafe.h"]
}
DriverConfig::Umdf(_) => {
vec!["windows.h"]
}
}
}
#[tracing::instrument(level = "trace")]
fn wdf_headers(&self) -> Vec<&'static str> {
if matches!(
self.driver_config,
DriverConfig::Kmdf(_) | DriverConfig::Umdf(_)
) {
vec!["wdf.h"]
} else {
vec![]
}
}
#[tracing::instrument(level = "trace")]
fn gpio_headers(&self) -> Vec<&'static str> {
let mut headers = vec!["gpio.h"];
if matches!(self.driver_config, DriverConfig::Kmdf(_)) {
headers.extend(["gpioclx.h"]);
}
headers
}
#[tracing::instrument(level = "trace")]
fn hid_headers(&self) -> Vec<&'static str> {
let mut headers = vec!["hidclass.h", "hidsdi.h", "hidpi.h", "vhf.h"];
if matches!(
self.driver_config,
DriverConfig::Wdm | DriverConfig::Kmdf(_)
) {
headers.extend(["hidpddi.h", "hidport.h", "kbdmou.h", "ntdd8042.h"]);
}
if matches!(self.driver_config, DriverConfig::Kmdf(_)) {
headers.extend(["HidSpiCx/1.0/hidspicx.h"]);
}
headers
}
#[tracing::instrument(level = "trace")]
fn parallel_ports_headers(&self) -> Vec<&'static str> {
let mut headers = vec!["ntddpar.h", "ntddser.h"];
if matches!(
self.driver_config,
DriverConfig::Wdm | DriverConfig::Kmdf(_)
) {
headers.extend(["parallel.h"]);
}
headers
}
#[tracing::instrument(level = "trace")]
fn spb_headers(&self) -> Vec<&'static str> {
let mut headers = vec!["spb.h", "reshub.h"];
if matches!(
self.driver_config,
DriverConfig::Wdm | DriverConfig::Kmdf(_)
) {
headers.extend(["pwmutil.h"]);
}
if matches!(self.driver_config, DriverConfig::Kmdf(_)) {
headers.extend(["spb/1.1/spbcx.h"]);
}
headers
}
#[tracing::instrument(level = "trace")]
fn storage_headers(&self) -> Vec<&'static str> {
let mut headers = vec![
"ehstorioctl.h",
"ntddcdrm.h",
"ntddcdvd.h",
"ntdddisk.h",
"ntddmmc.h",
"ntddscsi.h",
"ntddstor.h",
"ntddtape.h",
"ntddvol.h",
"ufs.h",
];
if matches!(
self.driver_config,
DriverConfig::Wdm | DriverConfig::Kmdf(_)
) {
headers.extend([
"mountdev.h",
"mountmgr.h",
"ntddchgr.h",
"ntdddump.h",
"storduid.h",
"storport.h",
]);
}
if matches!(self.driver_config, DriverConfig::Kmdf(_)) {
headers.extend(["ehstorbandmgmt.h"]);
}
headers
}
#[tracing::instrument(level = "trace")]
fn usb_headers(&self) -> Result<Vec<String>, ConfigError> {
let mut headers = Vec::new();
headers.extend(
[
"usb.h",
"usbfnbase.h",
"usbioctl.h",
"usbspec.h",
"Usbpmapi.h",
]
.iter()
.map(ToString::to_string),
);
if matches!(
self.driver_config,
DriverConfig::Wdm | DriverConfig::Kmdf(_)
) {
headers.extend(
["usbbusif.h", "usbdlib.h", "usbfnattach.h", "usbfnioctl.h"]
.iter()
.map(ToString::to_string),
);
}
if matches!(
self.driver_config,
DriverConfig::Kmdf(_) | DriverConfig::Umdf(_)
) {
headers.extend(["wdfusb.h".to_string()]);
}
if matches!(self.driver_config, DriverConfig::Kmdf(_)) {
headers.extend(
[
"ucm/1.0/UcmCx.h",
"UcmTcpci/1.0/UcmTcpciCx.h",
"UcmUcsi/1.0/UcmucsiCx.h",
"ude/1.1/UdeCx.h",
"ufx/1.1/ufxbase.h",
"ufxproprietarycharger.h",
"urs/1.0/UrsCx.h",
]
.iter()
.map(ToString::to_string),
);
let latest_ucx_header_path = self.ucx_header()?;
headers.push(latest_ucx_header_path);
if Self::should_include_ufxclient() {
headers.push("ufx/1.1/ufxclient.h".to_string());
}
}
Ok(headers)
}
/// Determines whether to include the ufxclient.h header based on the Clang
/// version used by bindgen.
///
/// The ufxclient.h header contains FORCEINLINE annotations that are invalid
/// according to the C standard. While MSVC silently ignores these in C
/// mode, older versions of Clang (pre-20.0) will error, even with MSVC
/// compatibility enabled.
///
/// This function checks if the current Clang version is 20.0 or newer,
/// where the issue was fixed. See
/// <https://github.com/llvm/llvm-project/issues/124869> for details.
#[tracing::instrument(level = "trace")]
fn should_include_ufxclient() -> bool {
const MINIMUM_CLANG_MAJOR_VERSION_WITH_INVALID_INLINE_FIX: u32 = 20;
let clang_version = ::bindgen::clang_version();
match clang_version.parsed {
Some((major, _minor))
if major >= MINIMUM_CLANG_MAJOR_VERSION_WITH_INVALID_INLINE_FIX =>
{
true
}
Some(_) => {
tracing::info!(
"Skipping ufxclient.h due to FORCEINLINE bug in {}",
clang_version.full
);
false
}
None => {
tracing::warn!(
"Failed to parse semver Major and Minor components from full Clang version \
string: {}",
clang_version.full
);
false
}
}
}
/// Returns a [`String`] containing the contents of a header file designed
/// for [`bindgen`](https://docs.rs/bindgen) to process
///
/// The contents contain `#include`'ed headers based off the [`ApiSubset`]
/// and [`Config`], as well as any additional definitions required for the
/// headers to be processed successfully.
///
/// # Errors
/// [`ConfigError`] - if the headers for a [`ApiSubset`] could not be
/// determined
#[tracing::instrument(level = "debug")]
pub fn bindgen_header_contents(
&self,
api_subsets: impl IntoIterator<Item = ApiSubset> + fmt::Debug,
) -> Result<String, ConfigError> {
Ok(api_subsets
.into_iter()
.map(|api_subset| self.headers(api_subset))
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.flat_map(|iter| iter.map(|header| format!("#include \"{header}\"\n")))
.collect())
}
/// Configure a Cargo build of a library that depends on the WDK. This
/// emits specially formatted prints to Cargo based on this [`Config`].
///
/// # Errors
///
/// This function will return an error if the [`Config`] fails to be
/// serialized
#[tracing::instrument(level = "debug")]
pub fn configure_library_build(&self) -> Result<(), ConfigError> {
self.emit_cfg_settings()
}
/// Compute the name of the `WdfFunctions` symbol used for WDF function
/// dispatching based off of the [`Config`]. Returns `None` if the driver
/// model is [`DriverConfig::Wdm`]
#[must_use]
#[tracing::instrument(level = "debug")]
pub fn compute_wdffunctions_symbol_name(&self) -> Option<String> {
let (wdf_major_version, wdf_minor_version) = match self.driver_config {
DriverConfig::Kmdf(config) => {
(config.kmdf_version_major, config.target_kmdf_version_minor)
}
DriverConfig::Umdf(config) => {
(config.umdf_version_major, config.target_umdf_version_minor)
}
DriverConfig::Wdm => return None,
};
Some(format!(
"WdfFunctions_{wdf_major_version:02}0{wdf_minor_version:02}"
))
}
/// Configure a Cargo build of a binary that depends on the WDK. This
/// emits specially formatted prints to Cargo based on this [`Config`].
///
/// This consists mainly of linker setting configuration. This must be
/// called from a Cargo build script of the binary being built
///
/// # Errors
///
/// This function will return an error if:
/// * any of the required WDK paths do not exist
/// * the C runtime is not configured to be statically linked for a
/// kernel-mode driver
///
/// # Panics
///
/// Panics if the invoked from outside a Cargo build environment
#[tracing::instrument(level = "debug")]
pub fn configure_binary_build(&self) -> Result<(), ConfigError> {
if !Self::is_crt_static_linked() {
cfg_if::cfg_if! {
if #[cfg(all(wdk_build_unstable, skip_umdf_static_crt_check))] {
if !matches!(self.driver_config, DriverConfig::Umdf(_)) {
return Err(ConfigError::StaticCrtNotEnabled);
}
} else {
return Err(ConfigError::StaticCrtNotEnabled);
}
};
}
// Emit linker search paths
for path in self.library_paths()? {
println!("cargo::rustc-link-search={}", path.display());
}
match &self.driver_config {
DriverConfig::Wdm => {
// Emit WDM-specific libraries to link to
println!("cargo::rustc-link-lib=static=BufferOverflowFastFailK");
println!("cargo::rustc-link-lib=static=ntoskrnl");
println!("cargo::rustc-link-lib=static=hal");
println!("cargo::rustc-link-lib=static=wmilib");
// Emit ARM64-specific libraries to link to derived from
// WindowsDriver.arm64.props
if self.cpu_architecture == CpuArchitecture::Arm64 {
println!("cargo::rustc-link-lib=static=arm64rt");
}
// Linker arguments derived from WindowsDriver.KernelMode.props in Ni(22H2) WDK
println!("cargo::rustc-cdylib-link-arg=/DRIVER");
println!("cargo::rustc-cdylib-link-arg=/NODEFAULTLIB");
println!("cargo::rustc-cdylib-link-arg=/SUBSYSTEM:NATIVE");
println!("cargo::rustc-cdylib-link-arg=/KERNEL");
// Linker arguments derived from WindowsDriver.KernelMode.WDM.props in Ni(22H2)
// WDK
println!("cargo::rustc-cdylib-link-arg=/ENTRY:DriverEntry");
// Ignore `LNK4257: object file was not compiled for kernel mode; the image
// might not run` since `rustc` has no support for `/KERNEL`
println!("cargo::rustc-cdylib-link-arg=/IGNORE:4257");
// Ignore `LNK4216: Exported entry point DriverEntry` since Rust currently
// provides no way to set a symbol's name without also exporting the symbol:
// https://github.com/rust-lang/rust/issues/67399
println!("cargo::rustc-cdylib-link-arg=/IGNORE:4216");
}
DriverConfig::Kmdf(_) => {
// Emit KMDF-specific libraries to link to
println!("cargo::rustc-link-lib=static=BufferOverflowFastFailK");
println!("cargo::rustc-link-lib=static=ntoskrnl");
println!("cargo::rustc-link-lib=static=hal");
println!("cargo::rustc-link-lib=static=wmilib");
println!("cargo::rustc-link-lib=static=WdfLdr");
println!("cargo::rustc-link-lib=static=WdfDriverEntry");
// Emit ARM64-specific libraries to link to derived from
// WindowsDriver.arm64.props
if self.cpu_architecture == CpuArchitecture::Arm64 {
println!("cargo::rustc-link-lib=static=arm64rt");
}
// Linker arguments derived from WindowsDriver.KernelMode.props in Ni(22H2) WDK
println!("cargo::rustc-cdylib-link-arg=/DRIVER");
println!("cargo::rustc-cdylib-link-arg=/NODEFAULTLIB");
println!("cargo::rustc-cdylib-link-arg=/SUBSYSTEM:NATIVE");
println!("cargo::rustc-cdylib-link-arg=/KERNEL");
// Linker arguments derived from WindowsDriver.KernelMode.KMDF.props in
// Ni(22H2) WDK
println!("cargo::rustc-cdylib-link-arg=/ENTRY:FxDriverEntry");
// Ignore `LNK4257: object file was not compiled for kernel mode; the image
// might not run` since `rustc` has no support for `/KERNEL`
println!("cargo::rustc-cdylib-link-arg=/IGNORE:4257");
}
DriverConfig::Umdf(umdf_config) => {
// Emit UMDF-specific libraries to link to
if umdf_config.umdf_version_major >= 2 {
println!("cargo::rustc-link-lib=static=WdfDriverStubUm");
println!("cargo::rustc-link-lib=static=ntdll");
}
println!("cargo::rustc-cdylib-link-arg=/NODEFAULTLIB:kernel32.lib");
println!("cargo::rustc-cdylib-link-arg=/NODEFAULTLIB:user32.lib");
println!("cargo::rustc-link-lib=static=OneCoreUAP");
// Linker arguments derived from WindowsDriver.UserMode.props in Ni(22H2) WDK
println!("cargo::rustc-cdylib-link-arg=/SUBSYSTEM:WINDOWS");
}
}
// Emit linker arguments common to all configs
{
// Linker arguments derived from Microsoft.Link.Common.props in Ni(22H2) WDK
println!("cargo::rustc-cdylib-link-arg=/NXCOMPAT");
println!("cargo::rustc-cdylib-link-arg=/DYNAMICBASE");
// Always generate Map file with Exports
println!("cargo::rustc-cdylib-link-arg=/MAP");
println!("cargo::rustc-cdylib-link-arg=/MAPINFO:EXPORTS");
// Force Linker Optimizations
println!("cargo::rustc-cdylib-link-arg=/OPT:REF,ICF");
// Enable "Forced Integrity Checking" to prevent non-signed binaries from
// loading
println!("cargo::rustc-cdylib-link-arg=/INTEGRITYCHECK");
// Disable Manifest File Generation
println!("cargo::rustc-cdylib-link-arg=/MANIFEST:NO");
}
self.emit_cfg_settings()
}
#[tracing::instrument(level = "trace")]
fn is_crt_static_linked() -> bool {
const STATICALLY_LINKED_C_RUNTIME_FEATURE_NAME: &str = "crt-static";
let enabled_cpu_target_features = env::var("CARGO_CFG_TARGET_FEATURE")
.expect("CARGO_CFG_TARGET_FEATURE should be set by Cargo");
enabled_cpu_target_features.contains(STATICALLY_LINKED_C_RUNTIME_FEATURE_NAME)
}
/// Constructs the architecture-specific Windows SDK library path using the
/// provided SDK Version and the driver configuration.
///
/// Builds the library path following the Windows SDK convention:
/// `{library_directory}/{sdk_version}/{km|um}/{architecture}/`
///
/// # Arguments
///
/// * `sdk_version` - Windows SDK version string (e.g., "10.0.22621.0")
///
/// # Returns
///
/// The constructed library path if it exists, otherwise
/// `ConfigError::DirectoryNotFound`.
///
/// # Examples
///
/// KMDF/AMD64: `C:\...\Lib\10.0.22621.0\km\x64`
/// UMDF/ARM64: `C:\...\Lib\10.0.22621.0\um\arm64`
#[tracing::instrument(level = "trace")]
fn sdk_library_path(&self, sdk_version: String) -> Result<PathBuf, ConfigError> {
let windows_sdk_library_path =
self.wdk_content_root
.join("Lib")
.join(sdk_version)
.join(match self.driver_config {
DriverConfig::Wdm | DriverConfig::Kmdf(_) => {
format!("km/{}", self.cpu_architecture.as_windows_str(),)
}
DriverConfig::Umdf(_) => {
format!("um/{}", self.cpu_architecture.as_windows_str(),)
}
});
if !windows_sdk_library_path.is_dir() {
return Err(ConfigError::DirectoryNotFound {
directory: windows_sdk_library_path.to_string_lossy().into(),
});
}
Ok(windows_sdk_library_path)
}
/// Returns the path to the latest available UCX header file present in the
/// Lib folder of the WDK content root
#[tracing::instrument(level = "trace")]
fn ucx_header(&self) -> Result<String, ConfigError> {
let sdk_version = utils::detect_windows_sdk_version(&self.wdk_content_root)?;
let ucx_header_root_dir = self.sdk_library_path(sdk_version)?.join("ucx");
let max_version = utils::find_max_version_in_directory(&ucx_header_root_dir)?;
let path = format!("ucx/{}.{}/ucxclass.h", max_version.0, max_version.1);
Ok(path)
}
}
impl From<DeserializableDriverConfig> for DriverConfig {
fn from(config: DeserializableDriverConfig) -> Self {
match config {
DeserializableDriverConfig::Wdm => Self::Wdm,
DeserializableDriverConfig::Kmdf(kmdf_config) => Self::Kmdf(kmdf_config),
DeserializableDriverConfig::Umdf(umdf_config) => Self::Umdf(umdf_config),
}
}
}
impl Default for KmdfConfig {
fn default() -> Self {
// FIXME: determine default values from TargetVersion and _NT_TARGET_VERSION
Self {
kmdf_version_major: 1,
target_kmdf_version_minor: 33,
minimum_kmdf_version_minor: None,
}
}
}
impl KmdfConfig {
/// Creates a new [`KmdfConfig`] with default values
#[must_use]
pub fn new() -> Self {
Self::default()
}
}
impl Default for UmdfConfig {
fn default() -> Self {
// FIXME: determine default values from TargetVersion and _NT_TARGET_VERSION
Self {
umdf_version_major: 2,
target_umdf_version_minor: 33,
minimum_umdf_version_minor: None,
}
}
}
impl UmdfConfig {
/// Creates a new [`UmdfConfig`] with default values
#[must_use]
pub fn new() -> Self {
Self::default()
}
}
impl CpuArchitecture {
/// Converts [`CpuArchitecture`] to the string corresponding to what the
/// architecture is typically referred to in Windows
#[must_use]
pub const fn as_windows_str(&self) -> &str {
match self {
Self::Amd64 => "x64",
Self::Arm64 => "ARM64",
}
}
/// Converts from a cargo-provided [`std::str`] to a [`CpuArchitecture`].
#[must_use]
pub fn try_from_cargo_str<S: AsRef<str>>(cargo_str: S) -> Option<Self> {
// Specifically not using the [`std::convert::TryFrom`] trait to be more
// explicit in function name, since only arch strings from cargo are handled.
match cargo_str.as_ref() {
"x86_64" => Some(Self::Amd64),
"aarch64" => Some(Self::Arm64),
_ => None,
}
}
}
/// Find the path of the toplevel Cargo manifest of the currently executing
/// Cargo subcommand. This should resolve to either:
/// 1. the `Cargo.toml` of the package where the Cargo subcommand (build, check,
/// etc.) was run
/// 2. the `Cargo.toml` provided to the `--manifest-path` argument to the Cargo
/// subcommand
/// 3. the `Cargo.toml` of the workspace that contains the package pointed to by
/// 1 or 2
///
/// The returned path should be a manifest in the same directory of the
/// lockfile. This does not support invocations that use non-default target
/// directories (ex. via `--target-dir`). This function only works when called
/// from a `build.rs` file
///
/// # Panics
///
/// Panics if a `Cargo.lock` file cannot be found in any of the ancestors of
/// `OUT_DIR` or if this function was called outside of a `build.rs` file
#[must_use]
#[tracing::instrument(level = "debug")]
pub fn find_top_level_cargo_manifest() -> PathBuf {
let out_dir =
PathBuf::from(env::var("OUT_DIR").expect(
"Cargo should have set the OUT_DIR environment variable when executing build.rs",
));
out_dir
.ancestors()
.find(|path| path.join("Cargo.lock").exists())
.expect("a Cargo.lock file should exist in the same directory as the top-level Cargo.toml")
.join("Cargo.toml")
}
/// Configure a Cargo build of a library that depends on the WDK.
///
/// This emits specially formatted prints to Cargo based on the [`Config`]
/// derived from `metadata.wdk` sections of `Cargo.toml`s.
///
/// Cargo build graphs that have no valid WDK configurations will emit a
/// warning, but will still return [`Ok`]. This allows libraries
/// designed for multiple configurations to successfully compile when built in
/// isolation.
///
/// # Errors
///
/// This function will return an error if the [`Config`] fails to be
/// serialized
#[tracing::instrument(level = "debug")]
pub fn configure_wdk_library_build() -> Result<(), ConfigError> {
match Config::from_env_auto() {
Ok(config) => {
config.configure_library_build()?;
Ok(())
}
Err(ConfigError::TryFromCargoMetadataError(
TryFromCargoMetadataError::NoWdkConfigurationsDetected,
)) => {
// No WDK configurations will be detected if the crate is not being used in a
// driver. Since this is usually the case when libraries are being built
// standalone, this scenario is treated as a warning.
tracing::warn!("No WDK configurations detected.");
// check_cfg must be emitted even if no WDK configurations are detected, so that
// cfg options are still checked
Config::emit_check_cfg_settings();
Ok(())
}
Err(error) => Err(error),
}
}
/// Configure a Cargo build of a library that depends on the WDK, then execute a
/// function or closure with the [`Config`] derived from `metadata.wdk` sections
/// of `Cargo.toml`s.
///
/// This emits specially formatted prints to Cargo based on the [`Config`]
/// derived from `metadata.wdk` sections of `Cargo.toml`s.
///
/// Cargo build graphs that have no valid WDK configurations will emit a
/// warning, but will still return [`Ok`]. This allows libraries
/// designed for multiple configurations to successfully compile when built in
/// isolation.
///
/// # Errors
///
/// This function will return an error if the [`Config`] fails to be
/// serialized
#[tracing::instrument(level = "debug", skip(f))]
pub fn configure_wdk_library_build_and_then<F, E>(mut f: F) -> Result<(), E>
where
F: FnMut(Config) -> Result<(), E>,
E: std::convert::From<ConfigError>,
{
match Config::from_env_auto() {
Ok(config) => {
config.configure_library_build()?;
debug!("Calling closure with {config:#?}");
Ok(f(config)?)
}
Err(ConfigError::TryFromCargoMetadataError(
TryFromCargoMetadataError::NoWdkConfigurationsDetected,
)) => {
// No WDK configurations will be detected if the crate is not being used in a
// driver. Since this is usually the case when libraries are being built
// standalone, this scenario is treated as a warning.
tracing::warn!("No WDK configurations detected.");
// check_cfg must be emitted even if no WDK configurations are detected, so that
// cfg options are still checked
Config::emit_check_cfg_settings();
Ok(())
}
Err(error) => Err(error.into()),
}
}
/// Configure a Cargo build of a binary that depends on the WDK using a
/// [`Config`] derived from `metadata.wdk` sections of `Cargo.toml`s.
///
/// # Errors
///
/// This function will return an error if:
/// * any of the required WDK paths do not exist
/// * the C runtime is not configured to be statically linked
///
/// # Panics
///
/// Panics if the invoked from outside a Cargo build environment
#[tracing::instrument(level = "debug")]
pub fn configure_wdk_binary_build() -> Result<(), ConfigError> {
Config::from_env_auto()?.configure_binary_build()
}
/// This currently only exports the driver type, but may export more metadata in
/// the future. `EXPORTED_CFG_SETTINGS` is a mapping of cfg key to allowed cfg
/// values
static EXPORTED_CFG_SETTINGS: LazyLock<Vec<(&'static str, Vec<&'static str>)>> =
LazyLock::new(|| vec![("DRIVER_MODEL-DRIVER_TYPE", vec!["WDM", "KMDF", "UMDF"])]);
/// Detects the WDK build number.
///
/// Detects the Windows Driver Kit (WDK) build number by locating
/// the WDK content root, retrieving the latest Windows SDK version, validating
/// the version format, and extracting the build number.
///
/// # Returns
///
/// Returns a `Result<u32, ConfigError>`, which contains the WDK
/// build number on success or a `ConfigError` on failure.
///
/// # Errors
///
/// Returns an error if:
/// * The WDK content root cannot be detected.
/// * The latest Windows SDK version cannot be retrieved.
/// * The WDK version string format is invalid.
/// * The WDK version number cannot be parsed.
///
/// # Panics
///
/// Panics if the WDK version number cannot be extracted from
/// the version string.
#[tracing::instrument(level = "debug")]
pub fn detect_wdk_build_number() -> Result<u32, ConfigError> {
let wdk_content_root =
utils::detect_wdk_content_root().ok_or(ConfigError::WdkContentRootDetectionError)?;
let detected_sdk_version = detect_windows_sdk_version(&wdk_content_root)?;
if !utils::validate_wdk_version_format(&detected_sdk_version) {
return Err(ConfigError::WdkVersionStringFormatError {
version: detected_sdk_version,
});
}
let wdk_build_number =
str::parse::<u32>(&utils::get_wdk_version_number(&detected_sdk_version)?).unwrap_or_else(
|_| panic!("Couldn't parse WDK version number! Version number: {detected_sdk_version}"),
);
Ok(wdk_build_number)
}
#[cfg(test)]
mod tests {
#[cfg(nightly_toolchain)]
use std::assert_matches::assert_matches;
use std::{collections::HashMap, ffi::OsStr, sync::Mutex};
use super::*;
use crate::utils::{remove_var, set_var};
mod two_part_version {
use super::*;
#[test]
fn valid_versions() {
assert_eq!("1.2".parse(), Ok(TwoPartVersion(1, 2)));
assert_eq!("0.0".parse(), Ok(TwoPartVersion(0, 0)));
assert_eq!("10.15".parse(), Ok(TwoPartVersion(10, 15)));
assert_eq!("999.1".parse(), Ok(TwoPartVersion(999, 1)));
assert_eq!("1.999".parse(), Ok(TwoPartVersion(1, 999)));
assert_eq!("01.02".parse(), Ok(TwoPartVersion(1, 2)));
assert_eq!("1.02".parse(), Ok(TwoPartVersion(1, 2)));
assert_eq!("01.2".parse(), Ok(TwoPartVersion(1, 2)));
}
#[test]
fn invalid_format_versions() {
assert_eq!(
String::new().parse::<TwoPartVersion>(),
Err(TwoPartVersionError::InvalidFormat(String::new()))
);
assert_eq!(
"1".parse::<TwoPartVersion>(),
Err(TwoPartVersionError::InvalidFormat("1".to_string()))
);
assert_eq!(
"123".parse::<TwoPartVersion>(),
Err(TwoPartVersionError::InvalidFormat("123".to_string()))
);
assert_eq!(
"1.2.3.4".parse::<TwoPartVersion>(),
Err(TwoPartVersionError::InvalidFormat("1.2.3.4".to_string()))
);
assert_eq!(
".".parse::<TwoPartVersion>(),
Err(TwoPartVersionError::InvalidFormat(".".to_string()))
);
assert_eq!(
".2".parse::<TwoPartVersion>(),
Err(TwoPartVersionError::InvalidFormat(".2".to_string()))
);
assert_eq!(
"1.".parse::<TwoPartVersion>(),
Err(TwoPartVersionError::InvalidFormat("1.".to_string()))
);
assert_eq!(
"myfolder".parse::<TwoPartVersion>(),
Err(TwoPartVersionError::InvalidFormat("myfolder".to_string()))
);
}
#[test]
fn parse_error_versions() {
assert_eq!(
"a.b".parse::<TwoPartVersion>(),
Err(TwoPartVersionError::ParseError(
"major".to_string(),
"a.b".to_string()
))
);
assert_eq!(
"1.b".parse::<TwoPartVersion>(),
Err(TwoPartVersionError::ParseError(
"minor".to_string(),
"1.b".to_string()
))
);
assert_eq!(
"a.2".parse::<TwoPartVersion>(),
Err(TwoPartVersionError::ParseError(
"major".to_string(),
"a.2".to_string()
))
);
assert_eq!(
"1.2a".parse::<TwoPartVersion>(),
Err(TwoPartVersionError::ParseError(
"minor".to_string(),
"1.2a".to_string()
))
);
assert_eq!(
"1a.2".parse::<TwoPartVersion>(),
Err(TwoPartVersionError::ParseError(
"major".to_string(),
"1a.2".to_string()
))
);
assert_eq!(
" 1.2".parse::<TwoPartVersion>(),
Err(TwoPartVersionError::ParseError(
"major".to_string(),
" 1.2".to_string()
))
);
assert_eq!(
"1.2 ".parse::<TwoPartVersion>(),
Err(TwoPartVersionError::ParseError(
"minor".to_string(),
"1.2 ".to_string()
))
);
assert_eq!(
"1 .2".parse::<TwoPartVersion>(),
Err(TwoPartVersionError::ParseError(
"major".to_string(),
"1 .2".to_string()
))
);
assert_eq!(
"1. 2".parse::<TwoPartVersion>(),
Err(TwoPartVersionError::ParseError(
"minor".to_string(),
"1. 2".to_string()
))
);
}
#[test]
fn version_ordering() {
let v1_0 = TwoPartVersion(1, 0);
let v1_1 = TwoPartVersion(1, 1);
let v1_999 = TwoPartVersion(1, 999);
let v2_0 = TwoPartVersion(2, 0);
let v2_1 = TwoPartVersion(2, 1);
assert!(v1_0 < v1_1);
assert!(v1_1 < v1_999);
assert!(v1_999 < v2_0);
assert!(v2_0 < v2_1);
}
#[test]
fn equality() {
let v1 = TwoPartVersion(1, 2);
let v2 = TwoPartVersion(1, 2);
let v3 = TwoPartVersion(1, 3);
assert_eq!(v1, v2);
assert_ne!(v1, v3);
}
#[test]
fn debug_formatting() {
let version = TwoPartVersion(1, 2);
let debug_str = format!("{version:?}");
assert_eq!(debug_str, "TwoPartVersion(1, 2)");
}
#[test]
fn max_selection() {
let versions = [
TwoPartVersion(1, 2),
TwoPartVersion(1, 10),
TwoPartVersion(2, 0),
TwoPartVersion(1, 5),
TwoPartVersion(2, 1),
TwoPartVersion(1, 999),
];
let max_version = versions.iter().max().unwrap();
assert_eq!(*max_version, TwoPartVersion(2, 1));
}
#[test]
fn u32_max_and_overflow() {
assert_eq!(
"4294967295.4294967295".parse::<TwoPartVersion>(),
Ok(TwoPartVersion(4_294_967_295, 4_294_967_295))
);
assert_eq!(
"4294967296.0".parse::<TwoPartVersion>(),
Err(TwoPartVersionError::ParseError(
"major".to_string(),
"4294967296.0".to_string()
))
);
assert_eq!(
"99999999999999999999.0".parse::<TwoPartVersion>(),
Err(TwoPartVersionError::ParseError(
"major".to_string(),
"99999999999999999999.0".to_string()
))
);
assert_eq!(
"0.4294967296".parse::<TwoPartVersion>(),
Err(TwoPartVersionError::ParseError(
"minor".to_string(),
"0.4294967296".to_string()
))
);
assert_eq!(
"1.99999999999999999999".parse::<TwoPartVersion>(),
Err(TwoPartVersionError::ParseError(
"minor".to_string(),
"1.99999999999999999999".to_string()
))
);
assert_eq!(
"4294967296.4294967296".parse::<TwoPartVersion>(),
Err(TwoPartVersionError::ParseError(
"major".to_string(),
"4294967296.4294967296".to_string()
))
);
}
}
/// Runs function after modifying environment variables, and returns the
/// function's return value.
///
/// The environment is guaranteed to be not modified during the execution
/// of the function, and the environment is reset to its original state
/// after execution of the function. No testing asserts should be called in
/// the function, since a failing test will poison the mutex, and cause all
/// remaining tests to fail.
///
/// # Panics
///
/// Panics if called with duplicate environment variable keys.
pub fn with_env<K, V, F, R>(env_vars_key_value_pairs: &[(K, V)], f: F) -> R
where
K: AsRef<OsStr> + std::cmp::Eq + std::hash::Hash,
V: AsRef<OsStr>,
F: FnOnce() -> R,
{
// Tests can execute in multiple threads in the same process, so mutex must be
// used to guard access to the environment variables
static ENV_MUTEX: Mutex<()> = Mutex::new(());
let _mutex_guard = ENV_MUTEX.lock().unwrap();
let mut original_env_vars = HashMap::new();
// set requested environment variables
for (key, value) in env_vars_key_value_pairs {
if let Ok(original_value) = env::var(key) {
let insert_result = original_env_vars.insert(key, original_value);
assert!(
insert_result.is_none(),
"Duplicate environment variable keys were provided"
);
}
set_var(key, value);
}
let f_return_value = f();
// reset all set environment variables
for (key, _) in env_vars_key_value_pairs {
original_env_vars.get(key).map_or_else(
|| {
remove_var(key);
},
|value| {
set_var(key, value);
},
);
}
f_return_value
}
#[test]
fn default_config() {
let config = with_env(&[("CARGO_CFG_TARGET_ARCH", "x86_64")], Config::new);
#[cfg(nightly_toolchain)]
assert_matches!(config.driver_config, DriverConfig::Wdm);
assert_eq!(config.cpu_architecture, CpuArchitecture::Amd64);
}
#[test]
fn wdm_config() {
let config = with_env(&[("CARGO_CFG_TARGET_ARCH", "x86_64")], || Config {
driver_config: DriverConfig::Wdm,
..Config::default()
});
#[cfg(nightly_toolchain)]
assert_matches!(config.driver_config, DriverConfig::Wdm);
assert_eq!(config.cpu_architecture, CpuArchitecture::Amd64);
}
#[test]
fn default_kmdf_config() {
let config = with_env(&[("CARGO_CFG_TARGET_ARCH", "x86_64")], || Config {
driver_config: DriverConfig::Kmdf(KmdfConfig::new()),
..Config::default()
});
#[cfg(nightly_toolchain)]
assert_matches!(
config.driver_config,
DriverConfig::Kmdf(KmdfConfig {
kmdf_version_major: 1,
target_kmdf_version_minor: 33,
minimum_kmdf_version_minor: None
})
);
assert_eq!(config.cpu_architecture, CpuArchitecture::Amd64);
}
#[test]
fn kmdf_config() {
let config = with_env(&[("CARGO_CFG_TARGET_ARCH", "x86_64")], || Config {
driver_config: DriverConfig::Kmdf(KmdfConfig {
kmdf_version_major: 1,
target_kmdf_version_minor: 15,
minimum_kmdf_version_minor: None,
}),
..Config::default()
});
#[cfg(nightly_toolchain)]
assert_matches!(
config.driver_config,
DriverConfig::Kmdf(KmdfConfig {
kmdf_version_major: 1,
target_kmdf_version_minor: 15,
minimum_kmdf_version_minor: None
})
);
assert_eq!(config.cpu_architecture, CpuArchitecture::Amd64);
}
#[test]
fn default_umdf_config() {
let config = with_env(&[("CARGO_CFG_TARGET_ARCH", "x86_64")], || Config {
driver_config: DriverConfig::Umdf(UmdfConfig::new()),
..Config::default()
});
#[cfg(nightly_toolchain)]
assert_matches!(
config.driver_config,
DriverConfig::Umdf(UmdfConfig {
umdf_version_major: 2,
target_umdf_version_minor: 33,
minimum_umdf_version_minor: None
})
);
assert_eq!(config.cpu_architecture, CpuArchitecture::Amd64);
}
#[test]
fn umdf_config() {
let config = with_env(&[("CARGO_CFG_TARGET_ARCH", "aarch64")], || Config {
driver_config: DriverConfig::Umdf(UmdfConfig {
umdf_version_major: 2,
target_umdf_version_minor: 15,
minimum_umdf_version_minor: None,
}),
..Config::default()
});
#[cfg(nightly_toolchain)]
assert_matches!(
config.driver_config,
DriverConfig::Umdf(UmdfConfig {
umdf_version_major: 2,
target_umdf_version_minor: 15,
minimum_umdf_version_minor: None
})
);
assert_eq!(config.cpu_architecture, CpuArchitecture::Arm64);
}
#[test]
fn test_try_from_cargo_str() {
assert_eq!(
CpuArchitecture::try_from_cargo_str("x86_64"),
Some(CpuArchitecture::Amd64)
);
assert_eq!(
CpuArchitecture::try_from_cargo_str("aarch64"),
Some(CpuArchitecture::Arm64)
);
assert_eq!(CpuArchitecture::try_from_cargo_str("arm"), None);
}
mod bindgen_header_contents {
use super::*;
use crate::{KmdfConfig, UmdfConfig};
#[test]
fn wdm() {
let config = with_env(&[("CARGO_CFG_TARGET_ARCH", "x86_64")], || Config {
driver_config: DriverConfig::Wdm,
..Default::default()
});
assert_eq!(
config.bindgen_header_contents([ApiSubset::Base]).unwrap(),
r#"#include "ntifs.h"
#include "ntddk.h"
#include "ntstrsafe.h"
"#,
);
}
#[test]
fn kmdf() {
let config = with_env(&[("CARGO_CFG_TARGET_ARCH", "x86_64")], || Config {
driver_config: DriverConfig::Kmdf(KmdfConfig {
kmdf_version_major: 1,
target_kmdf_version_minor: 33,
minimum_kmdf_version_minor: None,
}),
..Default::default()
});
assert_eq!(
config
.bindgen_header_contents([ApiSubset::Base, ApiSubset::Wdf])
.unwrap(),
r#"#include "ntifs.h"
#include "ntddk.h"
#include "ntstrsafe.h"
#include "wdf.h"
"#,
);
}
#[test]
fn umdf() {
let config = with_env(&[("CARGO_CFG_TARGET_ARCH", "aarch64")], || Config {
driver_config: DriverConfig::Umdf(UmdfConfig {
umdf_version_major: 2,
target_umdf_version_minor: 15,
minimum_umdf_version_minor: None,
}),
..Default::default()
});
assert_eq!(
config
.bindgen_header_contents([ApiSubset::Base, ApiSubset::Wdf])
.unwrap(),
r#"#include "windows.h"
#include "wdf.h"
"#,
);
}
}
mod compute_wdffunctions_symbol_name {
use super::*;
use crate::{KmdfConfig, UmdfConfig};
#[test]
fn kmdf() {
let config = with_env(&[("CARGO_CFG_TARGET_ARCH", "x86_64")], || Config {
driver_config: DriverConfig::Kmdf(KmdfConfig {
kmdf_version_major: 1,
target_kmdf_version_minor: 15,
minimum_kmdf_version_minor: None,
}),
..Default::default()
});
let result = config.compute_wdffunctions_symbol_name();
assert_eq!(result, Some("WdfFunctions_01015".to_string()));
}
#[test]
fn umdf() {
let config = with_env(&[("CARGO_CFG_TARGET_ARCH", "aarch64")], || Config {
driver_config: DriverConfig::Umdf(UmdfConfig {
umdf_version_major: 2,
target_umdf_version_minor: 33,
minimum_umdf_version_minor: None,
}),
..Default::default()
});
let result = config.compute_wdffunctions_symbol_name();
assert_eq!(result, Some("WdfFunctions_02033".to_string()));
}
#[test]
fn wdm() {
let config = with_env(&[("CARGO_CFG_TARGET_ARCH", "x86_64")], || Config {
driver_config: DriverConfig::Wdm,
..Default::default()
});
let result = config.compute_wdffunctions_symbol_name();
assert_eq!(result, None);
}
}
mod validate_and_add_folder_path {
use assert_fs::prelude::*;
use super::*;
#[test]
fn valid_directory_is_added_successfully() {
let temp_dir = assert_fs::TempDir::new().unwrap();
let mut include_paths = Vec::new();
let result = Config::validate_and_add_folder_path(&mut include_paths, temp_dir.path());
assert!(result.is_ok());
assert_eq!(include_paths.len(), 1);
assert!(include_paths[0].exists());
assert!(include_paths[0].is_dir());
// Verify the exact canonicalized path was added
let expected_path = absolute(temp_dir.path()).unwrap();
assert_eq!(include_paths[0], expected_path);
}
#[test]
fn non_existent_path_returns_directory_not_found_error() {
let non_existent_path = std::path::Path::new("/this/path/does/not/exist");
let mut include_paths = Vec::new();
let result =
Config::validate_and_add_folder_path(&mut include_paths, non_existent_path);
assert!(result.is_err());
#[cfg(nightly_toolchain)]
assert_matches!(
result.unwrap_err(),
ConfigError::DirectoryNotFound { directory } if directory == non_existent_path.to_string_lossy()
);
assert_eq!(include_paths.len(), 0);
}
#[test]
fn file_path_returns_directory_not_found_error() {
let temp_dir = assert_fs::TempDir::new().unwrap();
let file = temp_dir.child("test_file.txt");
file.write_str("test content").unwrap();
let mut include_paths = Vec::new();
let result = Config::validate_and_add_folder_path(&mut include_paths, file.path());
assert!(result.is_err());
#[cfg(nightly_toolchain)]
assert_matches!(
result.unwrap_err(),
ConfigError::DirectoryNotFound { directory } if directory == file.path().to_string_lossy()
);
assert_eq!(include_paths.len(), 0);
}
#[test]
fn path_is_canonicalized_before_adding() {
let temp_dir = assert_fs::TempDir::new().unwrap();
let sub_dir = temp_dir.child("subdir");
sub_dir.create_dir_all().unwrap();
// Create a path with ".." to test canonicalization
let complex_path = sub_dir.path().join("..").join("subdir");
let mut include_paths = Vec::new();
let result = Config::validate_and_add_folder_path(&mut include_paths, &complex_path);
assert!(result.is_ok());
assert_eq!(include_paths.len(), 1);
// The canonicalized path should not contain ".."
assert!(!include_paths[0].to_string_lossy().contains(".."));
assert!(include_paths[0].is_absolute());
// Verify the path resolves to the actual subdir path
let expected_path = absolute(sub_dir.path()).unwrap();
assert_eq!(include_paths[0], expected_path);
}
#[test]
fn multiple_paths_are_added_correctly() {
let temp_dir = assert_fs::TempDir::new().unwrap();
let dir1 = temp_dir.child("dir1");
let dir2 = temp_dir.child("dir2");
dir1.create_dir_all().unwrap();
dir2.create_dir_all().unwrap();
let mut include_paths = Vec::new();
let result1 = Config::validate_and_add_folder_path(&mut include_paths, dir1.path());
let result2 = Config::validate_and_add_folder_path(&mut include_paths, dir2.path());
assert!(result1.is_ok());
assert!(result2.is_ok());
assert_eq!(include_paths.len(), 2);
// Both paths should be present and different
assert_ne!(include_paths[0], include_paths[1]);
assert!(include_paths[0].exists());
assert!(include_paths[1].exists());
// Verify both paths match their expected canonicalized values
let expected_path1 = absolute(dir1.path()).unwrap();
let expected_path2 = absolute(dir2.path()).unwrap();
assert_eq!(include_paths[0], expected_path1);
assert_eq!(include_paths[1], expected_path2);
}
#[test]
fn nested_directory_is_handled_correctly() {
let temp_dir = assert_fs::TempDir::new().unwrap();
let nested_dir = temp_dir.child("level1").child("level2").child("level3");
nested_dir.create_dir_all().unwrap();
let mut include_paths = Vec::new();
let result =
Config::validate_and_add_folder_path(&mut include_paths, nested_dir.path());
assert!(result.is_ok());
assert_eq!(include_paths.len(), 1);
assert!(include_paths[0].exists());
assert!(include_paths[0].is_dir());
// Verify the nested path matches the expected canonicalized value
let expected_path = absolute(nested_dir.path()).unwrap();
assert_eq!(include_paths[0], expected_path);
}
#[test]
fn same_directory_can_be_added_multiple_times() {
let temp_dir = assert_fs::TempDir::new().unwrap();
let mut include_paths = Vec::new();
let result1 = Config::validate_and_add_folder_path(&mut include_paths, temp_dir.path());
let result2 = Config::validate_and_add_folder_path(&mut include_paths, temp_dir.path());
assert!(result1.is_ok());
assert!(result2.is_ok());
assert_eq!(include_paths.len(), 2);
assert_eq!(include_paths[0], include_paths[1]);
// Verify both entries match the expected canonicalized path
let expected_path = absolute(temp_dir.path()).unwrap();
assert_eq!(include_paths[0], expected_path);
assert_eq!(include_paths[1], expected_path);
}
#[cfg(windows)]
#[test]
fn windows_extended_length_paths_are_stripped() {
let temp_dir = assert_fs::TempDir::new().unwrap();
let mut include_paths = Vec::new();
let result = Config::validate_and_add_folder_path(&mut include_paths, temp_dir.path());
assert!(result.is_ok());
assert_eq!(include_paths.len(), 1);
// `validate_and_add_folder_path` should always ensure that the path should not
// start with \\?\ on Windows
let path_str = include_paths[0].to_string_lossy();
assert!(!path_str.starts_with(r"\\?\"));
// Verify the path matches expected value
let expected_path = absolute(temp_dir.path()).unwrap();
assert_eq!(include_paths[0], expected_path);
}
}
}