From d3e2bdbdf08077dfeb913764c0828d5084270c1c Mon Sep 17 00:00:00 2001 From: Alan Ngo Date: Tue, 19 May 2026 21:09:32 -0700 Subject: [PATCH 01/17] driver file versioning via .rc file initial commit --- crates/wdk-build/src/lib.rs | 5 + crates/wdk-build/src/resource_compile.rs | 747 +++++++++++++++++++++++ 2 files changed, 752 insertions(+) create mode 100644 crates/wdk-build/src/resource_compile.rs diff --git a/crates/wdk-build/src/lib.rs b/crates/wdk-build/src/lib.rs index 710708e5b..0151e3e6a 100644 --- a/crates/wdk-build/src/lib.rs +++ b/crates/wdk-build/src/lib.rs @@ -24,6 +24,7 @@ use tracing::debug; pub mod cargo_make; pub mod metadata; +pub mod resource_compile; mod utils; @@ -313,6 +314,10 @@ rustflags = [\"-C\", \"target-feature=+crt-static\"] /// [`metadata::Wdk`] #[error(transparent)] SerdeError(#[from] metadata::Error), + + /// Error returned when version resource compilation fails + #[error(transparent)] + ResourceCompileError(#[from] resource_compile::ResourceCompileError), } /// Subset of APIs in the Windows Driver Kit diff --git a/crates/wdk-build/src/resource_compile.rs b/crates/wdk-build/src/resource_compile.rs new file mode 100644 index 000000000..3623f4d87 --- /dev/null +++ b/crates/wdk-build/src/resource_compile.rs @@ -0,0 +1,747 @@ +// Copyright (c) Microsoft Corporation +// License: MIT OR Apache-2.0 + +//! Version resource compilation for Windows driver binaries. +//! +//! This module generates and compiles a Windows `VERSIONINFO` resource (`.rc` +//! file) that gets linked into the driver binary. This embeds version metadata +//! (file version, product name, company, copyright, etc.) into the `.sys` or +//! `.dll` PE file, making it visible in Windows Explorer's file properties. +//! +//! # Usage +//! +//! Add version resource metadata to your driver's `Cargo.toml`: +//! +//! ```toml +//! [package.metadata.wdk.version-resource] +//! company-name = "Microsoft Corporation" +//! copyright = "Copyright (C) Microsoft Corporation. All rights reserved" +//! product-name = "Surface" +//! file-description = "My Driver" +//! ``` +//! +//! Then call [`compile_version_resource`] from your `build.rs`: +//! +//! ```rust,ignore +//! let config = wdk_build::Config::from_env_auto()?; +//! config.configure_binary_build()?; +//! wdk_build::resource_compile::compile_version_resource(&config)?; +//! ``` +//! +//! # Version Sourcing +//! +//! The version is determined by (in priority order): +//! 1. `WDK_BUILD_VERSION` environment variable (for CI pipelines) +//! 2. `CARGO_PKG_VERSION` (from `Cargo.toml` `[package]` version) +//! +//! Semver versions are mapped to 4-part Windows versions by appending `.0` +//! for the revision component. Prerelease suffixes (e.g. `-preview`) are +//! stripped. Each component must fit in a 16-bit word (0–65535). + +use std::{ + env, + fmt::Write as _, + fs, + path::{absolute, Path, PathBuf}, + process::Command, +}; + +use crate::{Config, ConfigError, DriverConfig}; + +/// Environment variable for overriding the driver version in CI pipelines. +/// +/// When set, this takes priority over `CARGO_PKG_VERSION`. The value should +/// be in the format `MAJOR.MINOR.PATCH` or `MAJOR.MINOR.PATCH.REVISION`. +/// A prerelease suffix (e.g. `-preview`) is stripped automatically. +const VERSION_ENV_VAR: &str = "WDK_BUILD_VERSION"; + +/// A parsed 4-part Windows version number. +/// +/// Windows `VERSIONINFO` resources use four 16-bit components: +/// `MAJOR.MINOR.PATCH.REVISION`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DriverVersion { + /// Major version number + pub major: u16, + /// Minor version number + pub minor: u16, + /// Patch/build version number + pub patch: u16, + /// Revision number + pub revision: u16, +} + +impl DriverVersion { + /// Format as a comma-separated string for the `VER_FILEVERSION` RC macro. + /// + /// Example: `1,2,3,0` + #[must_use] + pub fn as_rc_numeric(&self) -> String { + format!( + "{},{},{},{}", + self.major, self.minor, self.patch, self.revision + ) + } + + /// Format as a dot-separated string for the `VER_FILEVERSION_STR` RC macro. + /// + /// Example: `"1.2.3.0"` + #[must_use] + pub fn as_rc_string(&self) -> String { + format!( + "{}.{}.{}.{}", + self.major, self.minor, self.patch, self.revision + ) + } +} + +/// Metadata for the version resource, sourced from +/// `[package.metadata.wdk.version-resource]` in `Cargo.toml`. +#[derive(Debug, Clone)] +pub struct VersionResourceMetadata { + /// Company name (e.g. "Microsoft Corporation") + pub company_name: String, + /// Copyright string + pub copyright: String, + /// Product name (e.g. "Surface") + pub product_name: String, + /// File description shown in Explorer properties + pub file_description: String, + /// Internal name of the binary (e.g. "MyDriver.sys") + pub internal_name: Option, + /// Original filename of the binary + pub original_filename: Option, +} + +/// Errors specific to version resource compilation. +#[derive(Debug, thiserror::Error)] +pub enum ResourceCompileError { + /// A version string could not be parsed into a valid driver version. + #[error("invalid version string '{value}': {reason}")] + VersionParseError { + /// The version string that could not be parsed + value: String, + /// Description of why the parsing failed + reason: String, + }, + + /// The resource compiler (`rc.exe`) exited with a non-zero status. + #[error("rc.exe failed with {status}:\n{stderr}")] + CompilerFailed { + /// The exit status of `rc.exe` + status: std::process::ExitStatus, + /// The stderr output from `rc.exe` + stderr: String, + }, + + /// Required metadata is missing from `Cargo.toml`. + #[error("version resource metadata error: {detail}")] + MetadataError { + /// Description of the metadata problem + detail: String, + }, + + /// An I/O error occurred during resource compilation. + #[error("I/O error during resource compilation")] + IoError(#[from] std::io::Error), + + /// An error from the WDK build configuration. + #[error("WDK build configuration error during resource compilation")] + ConfigError(#[source] Box), +} + +/// Parse a version string into a [`DriverVersion`]. +/// +/// Accepts the following formats: +/// - `MAJOR.MINOR.PATCH` (revision defaults to 0) +/// - `MAJOR.MINOR.PATCH.REVISION` +/// - Semver with prerelease tag: `1.2.3-alpha` (prerelease suffix is stripped) +/// +/// Each component must be in the range `0..=65535`. +/// +/// # Errors +/// +/// Returns [`ResourceCompileError::VersionParseError`] if the string cannot +/// be parsed or any component exceeds the 16-bit limit. +pub fn parse_version(version_str: &str) -> Result { + // Strip semver prerelease suffix (everything after first `-`) if present. + // e.g. "3.0.433-preview" → "3.0.433" + let version_clean = version_str.split('-').next().unwrap_or(version_str); + + let parts: Vec<&str> = version_clean.split('.').collect(); + + let (major, minor, patch, revision) = match parts.len() { + 3 => { + let major = parse_version_component(parts[0], "major", version_str)?; + let minor = parse_version_component(parts[1], "minor", version_str)?; + let patch = parse_version_component(parts[2], "patch", version_str)?; + (major, minor, patch, 0) + } + 4 => { + let major = parse_version_component(parts[0], "major", version_str)?; + let minor = parse_version_component(parts[1], "minor", version_str)?; + let patch = parse_version_component(parts[2], "patch", version_str)?; + let revision = parse_version_component(parts[3], "revision", version_str)?; + (major, minor, patch, revision) + } + _ => { + return Err(ResourceCompileError::VersionParseError { + value: version_str.to_string(), + reason: format!( + "expected 3 or 4 dot-separated components, found {}", + version_str.to_string() + ), + }); + } + }; + + Ok(DriverVersion { + major, + minor, + patch, + revision, + }) +} + +/// Parse a single version component string into a `u16`. +fn parse_version_component( + s: &str, + component_name: &str, + full_version: &str, +) -> Result { + s.parse() + .map_err(|_| ResourceCompileError::VersionParseError { + value: full_version.to_string(), + reason: format!("{component_name} component '{s}' is not a valid u16 (0-65535)"), + }) +} + +/// Determine the driver version to embed in the binary. +/// +/// Checks pipeline env var first, then falls back to +/// `CARGO_PKG_VERSION` env var (emitted by cargo). +fn resolve_version() -> Result { + let version_str = if let Ok(ci_version) = env::var(VERSION_ENV_VAR) { + ci_version + } else { + env::var("CARGO_PKG_VERSION").map_err(|_| ResourceCompileError::MetadataError { + detail: "CARGO_PKG_VERSION environment variable not set. This function must be \ + called from a Cargo build script." + .to_string(), + })? + }; + + parse_version(&version_str) +} + +/// Resolve the driver binary filename based on driver type and crate name. +fn resolve_driver_filename(config: &Config) -> String { + let crate_name = env::var("CARGO_PKG_NAME").unwrap_or_else(|_| "driver".to_string()); + // Cargo converts hyphens to underscores in artifact names + let artifact_name = crate_name.replace('-', "_"); + + let extension = match config.driver_config { + DriverConfig::Wdm | DriverConfig::Kmdf(_) => "sys", + DriverConfig::Umdf(_) => "dll", + }; + + format!("{artifact_name}.{extension}") +} + +/// Read version resource metadata from `Cargo.toml` +/// `[package.metadata.wdk.version-resource]` section. +/// +/// This reads the current package's `CARGO_MANIFEST_DIR/Cargo.toml` and +/// extracts the version-resource metadata using `cargo_metadata`. +/// +/// # Errors +/// +/// Returns [`ResourceCompileError::MetadataError`] if required fields are +/// missing. +pub fn read_version_resource_metadata() -> Result { + let manifest_dir = + env::var("CARGO_MANIFEST_DIR").map_err(|_| ResourceCompileError::MetadataError { + detail: "CARGO_MANIFEST_DIR not set. Must be called from a build script.".to_string(), + })?; + + let manifest_path = Path::new(&manifest_dir).join("Cargo.toml"); + + let metadata = cargo_metadata::MetadataCommand::new() + .manifest_path(&manifest_path) + .no_deps() + .exec() + .map_err(|e| ResourceCompileError::MetadataError { + detail: format!("cargo metadata failed: {e}"), + })?; + + // Find the current package + let pkg_name = env::var("CARGO_PKG_NAME").map_err(|_| ResourceCompileError::MetadataError { + detail: "CARGO_PKG_NAME not set".to_string(), + })?; + + let package = metadata + .packages + .iter() + .find(|p| p.name == pkg_name) + .ok_or_else(|| ResourceCompileError::MetadataError { + detail: format!("package '{pkg_name}' not found in cargo metadata"), + })?; + + let version_resource = package + .metadata + .get("wdk") + .and_then(|w| w.get("version-resource")) + .ok_or_else(|| ResourceCompileError::MetadataError { + detail: "[package.metadata.wdk.version-resource] section not found in Cargo.toml" + .to_string(), + })?; + + let company_name = version_resource + .get("company-name") + .and_then(serde_json::Value::as_str) + .ok_or_else(|| ResourceCompileError::MetadataError { + detail: "company-name is required in [package.metadata.wdk.version-resource]" + .to_string(), + })? + .to_string(); + + let copyright = version_resource + .get("copyright") + .and_then(serde_json::Value::as_str) + .ok_or_else(|| ResourceCompileError::MetadataError { + detail: "copyright is required in [package.metadata.wdk.version-resource]".to_string(), + })? + .to_string(); + + let product_name = version_resource + .get("product-name") + .and_then(serde_json::Value::as_str) + .ok_or_else(|| ResourceCompileError::MetadataError { + detail: "product-name is required in [package.metadata.wdk.version-resource]" + .to_string(), + })? + .to_string(); + + // file-description defaults to CARGO_PKG_DESCRIPTION + let file_description = version_resource + .get("file-description") + .and_then(serde_json::Value::as_str) + .map(ToString::to_string) + .or_else(|| env::var("CARGO_PKG_DESCRIPTION").ok()) + .unwrap_or_else(|| company_name.clone()); + + let internal_name = version_resource + .get("internal-name") + .and_then(serde_json::Value::as_str) + .map(ToString::to_string); + + let original_filename = version_resource + .get("original-filename") + .and_then(serde_json::Value::as_str) + .map(ToString::to_string); + + Ok(VersionResourceMetadata { + company_name, + copyright, + product_name, + file_description, + internal_name, + original_filename, + }) +} + +/// Generate the contents of a WDK-style `.rc` file. +/// +/// The generated file uses the standard Windows driver pattern: +/// `ntverp.h` + `common.ver` to emit a `VS_VERSION_INFO` resource block. +/// +/// # Arguments +/// +/// * `version` - The 4-part driver version to embed +/// * `metadata` - Version resource metadata (company, product, etc.) +/// * `config` - WDK build configuration (used to determine file type) +#[must_use] +pub fn generate_rc_content( + version: &DriverVersion, + metadata: &VersionResourceMetadata, + config: &Config, +) -> String { + let driver_filename = metadata + .internal_name + .clone() + .unwrap_or_else(|| resolve_driver_filename(config)); + + let original_filename = metadata + .original_filename + .clone() + .unwrap_or_else(|| driver_filename.clone()); + + // Determine file type based on driver model + let (ver_filetype, ver_filesubtype) = match config.driver_config { + DriverConfig::Wdm | DriverConfig::Kmdf(_) => ("VFT_DRV", "VFT2_DRV_SYSTEM"), + DriverConfig::Umdf(_) => ("VFT_DLL", "VFT2_UNKNOWN"), + }; + + let mut rc = String::with_capacity(1024); + writeln!(rc, "#include ").expect("write to String should not fail"); + writeln!(rc, "#include ").expect("write to String should not fail"); + writeln!(rc).expect("write to String should not fail"); + writeln!(rc, "#define VER_FILETYPE {ver_filetype}") + .expect("write to String should not fail"); + writeln!(rc, "#define VER_FILESUBTYPE {ver_filesubtype}") + .expect("write to String should not fail"); + writeln!(rc).expect("write to String should not fail"); + writeln!(rc, "#define VER_INTERNALNAME_STR \"{driver_filename}\"") + .expect("write to String should not fail"); + writeln!( + rc, + "#define VER_ORIGINALFILENAME_STR \"{original_filename}\"" + ) + .expect("write to String should not fail"); + writeln!(rc).expect("write to String should not fail"); + + // Use consistent #ifdef/#undef/#define pattern for all overridden macros + let file_description = &metadata.file_description; + let product_name = &metadata.product_name; + let copyright = &metadata.copyright; + let company_name = &metadata.company_name; + for (macro_name, macro_value) in [ + ("VER_FILEDESCRIPTION_STR", format!("\"{file_description}\"")), + ("VER_PRODUCTNAME_STR", format!("\"{product_name}\"")), + ("VER_FILEVERSION", version.as_rc_numeric()), + ( + "VER_FILEVERSION_STR", + format!("\"{}\"", version.as_rc_string()), + ), + ("VER_PRODUCTVERSION", "VER_FILEVERSION".to_string()), + ("VER_PRODUCTVERSION_STR", "VER_FILEVERSION_STR".to_string()), + ("VER_LEGALCOPYRIGHT_STR", format!("\"{copyright}\"")), + ("VER_COMPANYNAME_STR", format!("\"{company_name}\"")), + ] { + writeln!(rc, "#ifdef {macro_name}").expect("write to String should not fail"); + writeln!(rc, "#undef {macro_name}").expect("write to String should not fail"); + writeln!(rc, "#endif").expect("write to String should not fail"); + writeln!(rc, "#define {macro_name} {macro_value}") + .expect("write to String should not fail"); + writeln!(rc).expect("write to String should not fail"); + } + + writeln!(rc, "#include \"common.ver\"").expect("write to String should not fail"); + + rc +} + +/// Compute the include paths needed for resource compilation. +/// +/// Unlike [`Config::include_paths`] (which provides paths for C/bindgen +/// compilation), resource compilation needs: +/// - `Include//um` (for `windows.h`) +/// - `Include//shared` (for shared headers) +/// - `Include//km` (for `ntverp.h`, kernel-mode drivers only) +fn resource_include_paths(config: &Config) -> Result, ResourceCompileError> { + let sdk_version = crate::utils::detect_windows_sdk_version(&config.wdk_content_root) + .map_err(|e| ResourceCompileError::ConfigError(Box::new(e)))?; + let include_directory = config.wdk_content_root.join("Include").join(&sdk_version); + + let mut paths = vec![]; + + // um directory contains windows.h + let um_path = include_directory.join("um"); + if um_path.is_dir() { + paths.push(absolute(&um_path)?); + } + + // shared directory contains shared headers + let shared_path = include_directory.join("shared"); + if shared_path.is_dir() { + paths.push(absolute(&shared_path)?); + } + + // km directory contains ntverp.h (for kernel-mode drivers) + match config.driver_config { + DriverConfig::Wdm | DriverConfig::Kmdf(_) => { + let km_path = include_directory.join("km"); + if km_path.is_dir() { + paths.push(absolute(&km_path)?); + } + } + DriverConfig::Umdf(_) => {} + } + + Ok(paths) +} + +/// Generate and compile a Windows `VERSIONINFO` resource, then emit a +/// linker directive to embed it in the driver binary. +/// +/// This is the main entry point for version resource compilation. It: +/// 1. Reads version from `WDK_BUILD_VERSION` or `CARGO_PKG_VERSION` +/// 2. Reads metadata from `[package.metadata.wdk.version-resource]` +/// 3. Generates a WDK-style `.rc` file in `OUT_DIR` +/// 4. Compiles it with `rc.exe` +/// 5. Emits `cargo::rustc-cdylib-link-arg` to link the `.res` into the binary +/// +/// # Errors +/// +/// Returns [`ConfigError`] (wrapping [`ResourceCompileError`]) if: +/// - Version metadata is missing or invalid +/// - `rc.exe` cannot be found +/// - Resource compilation fails +/// +/// # Panics +/// +/// Panics if `OUT_DIR` is not set (i.e., not called from a build script). +pub fn compile_version_resource(config: &Config) -> Result<(), ConfigError> { + Ok(compile_version_resource_inner(config)?) +} + +/// Inner implementation that uses [`ResourceCompileError`] directly. +fn compile_version_resource_inner(config: &Config) -> Result<(), ResourceCompileError> { + // Emit rerun-if-env-changed for version-affecting variables + println!("cargo::rerun-if-env-changed={VERSION_ENV_VAR}"); + + let version = resolve_version()?; + let metadata = read_version_resource_metadata()?; + + let out_dir = + PathBuf::from(env::var("OUT_DIR").expect( + "Cargo should have set the OUT_DIR environment variable when executing build.rs", + )); + + let rc_path = out_dir.join("version.rc"); + let res_path = out_dir.join("version.res"); + + // Generate RC file content + let rc_content = generate_rc_content(&version, &metadata, config); + fs::write(&rc_path, &rc_content)?; + + // Invoke rc.exe (expected to be on PATH via eWDK prompt or cargo-wdk setup) + let include_paths = resource_include_paths(config)?; + + let mut cmd = Command::new("rc.exe"); + for path in &include_paths { + cmd.arg("/I"); + cmd.arg(path); + } + cmd.arg("/fo"); + cmd.arg(&res_path); + cmd.arg(&rc_path); + + let output = cmd.output()?; + + if !output.status.success() { + return Err(ResourceCompileError::CompilerFailed { + status: output.status, + stderr: String::from_utf8_lossy(&output.stderr).to_string(), + }); + } + + // Emit linker directive to include the compiled resource + println!("cargo::rustc-cdylib-link-arg={}", res_path.display()); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::CpuArchitecture; + + // ── Version parsing tests ────────────────────────────────────────── + + #[test] + fn parse_version_three_part() { + let v = parse_version("1.2.3").unwrap(); + assert_eq!( + v, + DriverVersion { + major: 1, + minor: 2, + patch: 3, + revision: 0 + } + ); + } + + #[test] + fn parse_version_four_part() { + let v = parse_version("1.2.3.4").unwrap(); + assert_eq!( + v, + DriverVersion { + major: 1, + minor: 2, + patch: 3, + revision: 4 + } + ); + } + + #[test] + fn parse_version_strips_prerelease() { + let v = parse_version("3.0.433-preview").unwrap(); + assert_eq!( + v, + DriverVersion { + major: 3, + minor: 0, + patch: 433, + revision: 0 + } + ); + } + + #[test] + fn parse_version_max_values() { + let v = parse_version("65535.65535.65535.65535").unwrap(); + assert_eq!( + v, + DriverVersion { + major: 65535, + minor: 65535, + patch: 65535, + revision: 65535 + } + ); + } + + #[test] + fn parse_version_overflow_rejected() { + let result = parse_version("65536.0.0"); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(matches!( + err, + ResourceCompileError::VersionParseError { .. } + )); + } + + #[test] + fn parse_version_too_few_parts() { + assert!(parse_version("1.2").is_err()); + } + + #[test] + fn parse_version_too_many_parts() { + assert!(parse_version("1.2.3.4.5").is_err()); + } + + #[test] + fn parse_version_empty_string() { + assert!(parse_version("").is_err()); + } + + #[test] + fn parse_version_non_numeric() { + assert!(parse_version("1.2.abc").is_err()); + } + + #[test] + fn parse_version_zero() { + let v = parse_version("0.0.0").unwrap(); + assert_eq!( + v, + DriverVersion { + major: 0, + minor: 0, + patch: 0, + revision: 0 + } + ); + } + + // ── DriverVersion formatting tests ──────────────────────────────── + + #[test] + fn windows_version_rc_numeric() { + let v = DriverVersion { + major: 1, + minor: 2, + patch: 3, + revision: 4, + }; + assert_eq!(v.as_rc_numeric(), "1,2,3,4"); + } + + #[test] + fn windows_version_rc_string() { + let v = DriverVersion { + major: 1, + minor: 2, + patch: 3, + revision: 4, + }; + assert_eq!(v.as_rc_string(), "1.2.3.4"); + } + + // ── RC template generation tests ─────────────────────────────────── + + /// Create a test `Config` without relying on `Config::default()` which + /// requires build-script env vars like `CARGO_CFG_TARGET_ARCH`. + fn test_config(driver_config: DriverConfig) -> Config { + Config { + wdk_content_root: PathBuf::from("C:\\fake\\wdk"), + cpu_architecture: CpuArchitecture::Amd64, + driver_config, + } + } + + #[test] + fn generate_rc_content_contains_expected_fields() { + let version = DriverVersion { + major: 1, + minor: 0, + patch: 0, + revision: 0, + }; + let metadata = VersionResourceMetadata { + company_name: "Test Corp".to_string(), + copyright: "Copyright Test".to_string(), + product_name: "Test Product".to_string(), + file_description: "Test Driver".to_string(), + internal_name: Some("test.sys".to_string()), + original_filename: Some("test.sys".to_string()), + }; + let config = test_config(DriverConfig::Kmdf(crate::KmdfConfig::default())); + + let rc = generate_rc_content(&version, &metadata, &config); + + assert!(rc.contains("#include ")); + assert!(rc.contains("#include ")); + assert!(rc.contains("#include \"common.ver\"")); + assert!(rc.contains("VFT_DRV")); + assert!(rc.contains("VFT2_DRV_SYSTEM")); + assert!(rc.contains("\"test.sys\"")); + assert!(rc.contains("\"Test Driver\"")); + assert!(rc.contains("\"Test Product\"")); + assert!(rc.contains("\"Test Corp\"")); + assert!(rc.contains("\"Copyright Test\"")); + assert!(rc.contains("1,0,0,0")); + assert!(rc.contains("\"1.0.0.0\"")); + } + + #[test] + fn generate_rc_content_umdf_uses_dll_type() { + let version = DriverVersion { + major: 1, + minor: 0, + patch: 0, + revision: 0, + }; + let metadata = VersionResourceMetadata { + company_name: "Test".to_string(), + copyright: "Copyright".to_string(), + product_name: "Product".to_string(), + file_description: "Driver".to_string(), + internal_name: None, + original_filename: None, + }; + let config = test_config(DriverConfig::Umdf(crate::UmdfConfig::default())); + + let rc = generate_rc_content(&version, &metadata, &config); + + assert!(rc.contains("VFT_DLL")); + assert!(rc.contains("VFT2_UNKNOWN")); + } +} From d25204cb0e643e823f6e8808f5d9b0fe6751ca10 Mon Sep 17 00:00:00 2001 From: Alan Ngo Date: Thu, 21 May 2026 16:53:52 -0700 Subject: [PATCH 02/17] redesign so PE resource versioning is automatic --- crates/wdk-build/src/lib.rs | 2 + crates/wdk-build/src/metadata/mod.rs | 145 +++++++++++++++++++---- crates/wdk-build/src/resource_compile.rs | 131 ++++++++++---------- 3 files changed, 184 insertions(+), 94 deletions(-) diff --git a/crates/wdk-build/src/lib.rs b/crates/wdk-build/src/lib.rs index 0151e3e6a..9c8619f2d 100644 --- a/crates/wdk-build/src/lib.rs +++ b/crates/wdk-build/src/lib.rs @@ -1229,6 +1229,8 @@ impl Config { println!("cargo::rustc-cdylib-link-arg=/MANIFEST:NO"); } + resource_compile::compile_version_resource(self)?; + self.emit_cfg_settings() } diff --git a/crates/wdk-build/src/metadata/mod.rs b/crates/wdk-build/src/metadata/mod.rs index 6ac98b50a..46323c799 100644 --- a/crates/wdk-build/src/metadata/mod.rs +++ b/crates/wdk-build/src/metadata/mod.rs @@ -120,23 +120,21 @@ fn parse_packages_wdk_metadata( ) -> std::result::Result, TryFromCargoMetadataError> { let wdk_metadata_configurations = packages .iter() - .filter_map(|package| match &package.metadata["wdk"] { - serde_json::Value::Null => None, - // When wdk section is empty, treat it as if it wasn't there. This is to allow for using - // empty wdk metadata sections to mark the package as a driver (ex. for detection in - // `package_driver_flow_condition_script`) - serde_json::Value::Object(map) if map.is_empty() => None, - wdk_metadata => Some(Wdk::deserialize(wdk_metadata).map_err(|err| { - TryFromCargoMetadataError::WdkMetadataDeserialization { - metadata_source: format!( - "{} for {} package", - stringify!(package.metadata["wdk"]), - package.name - ), - error_source: err, - } - })), - }) + .filter_map( + |package| match wdk_driver_model_metadata(&package.metadata["wdk"]) { + None => None, + Some(wdk_metadata) => Some(Wdk::deserialize(wdk_metadata).map_err(|err| { + TryFromCargoMetadataError::WdkMetadataDeserialization { + metadata_source: format!( + "{} for {} package", + stringify!(package.metadata["wdk"]), + package.name + ), + error_source: err, + } + })), + }, + ) .collect::, _>>()?; Ok(wdk_metadata_configurations) } @@ -144,15 +142,36 @@ fn parse_packages_wdk_metadata( fn parse_workspace_wdk_metadata( workspace_metadata: &serde_json::Value, ) -> std::result::Result, TryFromCargoMetadataError> { - Ok(match &workspace_metadata["wdk"] { + Ok( + match wdk_driver_model_metadata(&workspace_metadata["wdk"]) { + None => None, + Some(wdk_metadata) => Some(Wdk::deserialize(wdk_metadata).map_err(|err| { + TryFromCargoMetadataError::WdkMetadataDeserialization { + metadata_source: stringify!(workspace_metadata["wdk"]).to_string(), + error_source: err, + } + })?), + }, + ) +} + +fn wdk_driver_model_metadata(wdk_metadata: &serde_json::Value) -> Option { + match wdk_metadata { serde_json::Value::Null => None, - wdk_metadata => Some(Wdk::deserialize(wdk_metadata).map_err(|err| { - TryFromCargoMetadataError::WdkMetadataDeserialization { - metadata_source: stringify!(workspace_metadata["wdk"]).to_string(), - error_source: err, - } - })?), - }) + // When wdk section is empty, treat it as if it wasn't there. This is to allow for using + // empty wdk metadata sections to mark the package as a driver (ex. for detection in + // `package_driver_flow_condition_script`) + serde_json::Value::Object(map) if map.is_empty() => None, + // Only `metadata.wdk.driver-model` is part of the WDK driver configuration. + // Other `metadata.wdk.*` sections are allowed to coexist and are parsed by + // their owning features. + serde_json::Value::Object(map) => map.get("driver-model").map(|driver_model| { + let mut driver_model_metadata = serde_json::Map::new(); + driver_model_metadata.insert("driver-model".to_string(), driver_model.clone()); + serde_json::Value::Object(driver_model_metadata) + }), + wdk_metadata => Some(wdk_metadata.clone()), + } } pub(crate) fn iter_manifest_paths(metadata: Metadata) -> impl IntoIterator { @@ -198,6 +217,82 @@ mod tests { }); } + #[test] + fn version_resource_metadata_is_not_part_of_wdk_configuration() { + let cwd = PathBuf::from(TEST_ROOT_DIR); + let package_metadata = TestWdkMetadata( + r#" + { + "wdk": { + "driver-model": { + "driver-type": "KMDF", + "kmdf-version-major": 1, + "target-kmdf-version-minor": 33 + }, + "version-resource": { + "company-name": "Microsoft Corporation", + "copyright": "Copyright", + "product-name": "Surface" + } + } + } + "# + .to_string(), + ); + let (member_id, package) = + create_cargo_metadata_package(&cwd, "sample-kmdf", "0.0.1", Some(package_metadata)); + + set_up_and_assert(&cwd, &[package], &[member_id], None, |wdk| { + assert!(matches!( + wdk.unwrap().driver_model, + DriverConfig::Kmdf(KmdfConfig { + kmdf_version_major: 1, + target_kmdf_version_minor: 33, + minimum_kmdf_version_minor: None + }) + )); + }); + } + + #[test] + fn package_version_resource_metadata_can_use_workspace_wdk_configuration() { + let cwd = PathBuf::from(TEST_ROOT_DIR); + let package_metadata = TestWdkMetadata( + r#" + { + "wdk": { + "version-resource": { + "company-name": "Microsoft Corporation", + "copyright": "Copyright", + "product-name": "Surface" + } + } + } + "# + .to_string(), + ); + let (member_id, package) = + create_cargo_metadata_package(&cwd, "sample-kmdf", "0.0.1", Some(package_metadata)); + let workspace_metadata = create_cargo_metadata_wdk_metadata("KMDF", 1, 33); + + set_up_and_assert( + &cwd, + &[package], + &[member_id], + Some(workspace_metadata), + |wdk| { + assert!(matches!( + wdk.unwrap().driver_model, + DriverConfig::Kmdf(KmdfConfig { + kmdf_version_major: 1, + target_kmdf_version_minor: 33, + minimum_kmdf_version_minor: None + }) + )); + }, + ); + } + #[test] fn multiple_members_with_distinct_wdk_configurations() { let (member_id1, package1, _cwd) = init_kmdf_package_metadata(1, 33); diff --git a/crates/wdk-build/src/resource_compile.rs b/crates/wdk-build/src/resource_compile.rs index 3623f4d87..20c7d8f5b 100644 --- a/crates/wdk-build/src/resource_compile.rs +++ b/crates/wdk-build/src/resource_compile.rs @@ -10,7 +10,9 @@ //! //! # Usage //! -//! Add version resource metadata to your driver's `Cargo.toml`: +//! [`Config::configure_binary_build`][crate::Config::configure_binary_build] +//! automatically compiles and links a version resource. Add optional metadata +//! to your driver's `Cargo.toml` to override the Cargo-derived defaults: //! //! ```toml //! [package.metadata.wdk.version-resource] @@ -20,14 +22,6 @@ //! file-description = "My Driver" //! ``` //! -//! Then call [`compile_version_resource`] from your `build.rs`: -//! -//! ```rust,ignore -//! let config = wdk_build::Config::from_env_auto()?; -//! config.configure_binary_build()?; -//! wdk_build::resource_compile::compile_version_resource(&config)?; -//! ``` -//! //! # Version Sourcing //! //! The version is determined by (in priority order): @@ -42,7 +36,7 @@ use std::{ env, fmt::Write as _, fs, - path::{absolute, Path, PathBuf}, + path::{Path, PathBuf, absolute}, process::Command, }; @@ -53,7 +47,7 @@ use crate::{Config, ConfigError, DriverConfig}; /// When set, this takes priority over `CARGO_PKG_VERSION`. The value should /// be in the format `MAJOR.MINOR.PATCH` or `MAJOR.MINOR.PATCH.REVISION`. /// A prerelease suffix (e.g. `-preview`) is stripped automatically. -const VERSION_ENV_VAR: &str = "WDK_BUILD_VERSION"; +const VERSION_ENV_VAR: &str = "STAMPINF_VERSION"; /// A parsed 4-part Windows version number. /// @@ -95,8 +89,8 @@ impl DriverVersion { } } -/// Metadata for the version resource, sourced from -/// `[package.metadata.wdk.version-resource]` in `Cargo.toml`. +/// Metadata for the version resource, sourced from Cargo defaults and optional +/// `[package.metadata.wdk.version-resource]` overrides in `Cargo.toml`. #[derive(Debug, Clone)] pub struct VersionResourceMetadata { /// Company name (e.g. "Microsoft Corporation") @@ -134,7 +128,7 @@ pub enum ResourceCompileError { stderr: String, }, - /// Required metadata is missing from `Cargo.toml`. + /// Metadata is missing or invalid. #[error("version resource metadata error: {detail}")] MetadataError { /// Description of the metadata problem @@ -225,8 +219,8 @@ fn resolve_version() -> Result { ci_version } else { env::var("CARGO_PKG_VERSION").map_err(|_| ResourceCompileError::MetadataError { - detail: "CARGO_PKG_VERSION environment variable not set. This function must be \ - called from a Cargo build script." + detail: "CARGO_PKG_VERSION environment variable not set. This function must be called \ + from a Cargo build script." .to_string(), })? }; @@ -248,17 +242,18 @@ fn resolve_driver_filename(config: &Config) -> String { format!("{artifact_name}.{extension}") } -/// Read version resource metadata from `Cargo.toml` -/// `[package.metadata.wdk.version-resource]` section. +/// Read version resource metadata from Cargo defaults and optional +/// `[package.metadata.wdk.version-resource]` overrides. /// -/// This reads the current package's `CARGO_MANIFEST_DIR/Cargo.toml` and -/// extracts the version-resource metadata using `cargo_metadata`. +/// This reads the current package's `CARGO_MANIFEST_DIR/Cargo.toml`, extracts +/// version-resource metadata using `cargo_metadata`, and fills absent fields +/// from Cargo package environment variables. /// /// # Errors /// -/// Returns [`ResourceCompileError::MetadataError`] if required fields are -/// missing. -pub fn read_version_resource_metadata() -> Result { +/// Returns [`ResourceCompileError::MetadataError`] if metadata exists but is +/// invalid. +fn read_version_resource_metadata() -> Result { let manifest_dir = env::var("CARGO_MANIFEST_DIR").map_err(|_| ResourceCompileError::MetadataError { detail: "CARGO_MANIFEST_DIR not set. Must be called from a build script.".to_string(), @@ -290,55 +285,32 @@ pub fn read_version_resource_metadata() -> Result Result, + key: &str, +) -> Result, ResourceCompileError> { + let Some(value) = version_resource.and_then(|metadata| metadata.get(key)) else { + return Ok(None); + }; + + value + .as_str() + .map(ToString::to_string) + .map(Some) + .ok_or_else(|| ResourceCompileError::MetadataError { + detail: format!("[package.metadata.wdk.version-resource].{key} must be a string"), + }) +} + +fn env_var_non_empty(key: &str) -> Option { + env::var(key).ok().filter(|value| !value.is_empty()) +} + /// Generate the contents of a WDK-style `.rc` file. /// /// The generated file uses the standard Windows driver pattern: @@ -476,7 +469,8 @@ fn resource_include_paths(config: &Config) -> Result, ResourceCompi /// /// This is the main entry point for version resource compilation. It: /// 1. Reads version from `WDK_BUILD_VERSION` or `CARGO_PKG_VERSION` -/// 2. Reads metadata from `[package.metadata.wdk.version-resource]` +/// 2. Reads metadata from Cargo defaults and optional +/// `[package.metadata.wdk.version-resource]` overrides /// 3. Generates a WDK-style `.rc` file in `OUT_DIR` /// 4. Compiles it with `rc.exe` /// 5. Emits `cargo::rustc-cdylib-link-arg` to link the `.res` into the binary @@ -484,14 +478,14 @@ fn resource_include_paths(config: &Config) -> Result, ResourceCompi /// # Errors /// /// Returns [`ConfigError`] (wrapping [`ResourceCompileError`]) if: -/// - Version metadata is missing or invalid +/// - Version metadata is invalid /// - `rc.exe` cannot be found /// - Resource compilation fails /// /// # Panics /// /// Panics if `OUT_DIR` is not set (i.e., not called from a build script). -pub fn compile_version_resource(config: &Config) -> Result<(), ConfigError> { +pub(crate) fn compile_version_resource(config: &Config) -> Result<(), ConfigError> { Ok(compile_version_resource_inner(config)?) } @@ -545,7 +539,6 @@ fn compile_version_resource_inner(config: &Config) -> Result<(), ResourceCompile #[cfg(test)] mod tests { use super::*; - use crate::CpuArchitecture; // ── Version parsing tests ────────────────────────────────────────── From 9fbd5f66efc0538c097d8480ddddf1f7480c2157 Mon Sep 17 00:00:00 2001 From: Alan Ngo Date: Fri, 22 May 2026 13:32:34 -0700 Subject: [PATCH 03/17] update resolve_version functionality to treat empty string in the env var as the same as an absent env var, update documentation, update and add unit tests --- crates/wdk-build/src/resource_compile.rs | 691 +++++++++++++++++------ 1 file changed, 508 insertions(+), 183 deletions(-) diff --git a/crates/wdk-build/src/resource_compile.rs b/crates/wdk-build/src/resource_compile.rs index 20c7d8f5b..ee4987cea 100644 --- a/crates/wdk-build/src/resource_compile.rs +++ b/crates/wdk-build/src/resource_compile.rs @@ -24,8 +24,9 @@ //! //! # Version Sourcing //! -//! The version is determined by (in priority order): -//! 1. `WDK_BUILD_VERSION` environment variable (for CI pipelines) +//! The version is determined by CI pipeline env var with +//! cargo package version fallback: +//! 1. `STAMPINF_VERSION` environment variable (for CI pipelines) //! 2. `CARGO_PKG_VERSION` (from `Cargo.toml` `[package]` version) //! //! Semver versions are mapped to 4-part Windows versions by appending `.0` @@ -213,17 +214,19 @@ fn parse_version_component( /// Determine the driver version to embed in the binary. /// /// Checks pipeline env var first, then falls back to -/// `CARGO_PKG_VERSION` env var (emitted by cargo). +/// `CARGO_PKG_VERSION` env var (emitted by cargo) +/// if env var is not present or empty. fn resolve_version() -> Result { - let version_str = if let Ok(ci_version) = env::var(VERSION_ENV_VAR) { - ci_version - } else { - env::var("CARGO_PKG_VERSION").map_err(|_| ResourceCompileError::MetadataError { - detail: "CARGO_PKG_VERSION environment variable not set. This function must be called \ - from a Cargo build script." - .to_string(), - })? - }; + let version_str = env_var_non_empty(VERSION_ENV_VAR).map_or_else( + || { + env::var("CARGO_PKG_VERSION").map_err(|_| ResourceCompileError::MetadataError { + detail: "CARGO_PKG_VERSION environment variable not set. This function must be \ + called from a Cargo build script." + .to_string(), + }) + }, + Ok, + )?; parse_version(&version_str) } @@ -468,7 +471,7 @@ fn resource_include_paths(config: &Config) -> Result, ResourceCompi /// linker directive to embed it in the driver binary. /// /// This is the main entry point for version resource compilation. It: -/// 1. Reads version from `WDK_BUILD_VERSION` or `CARGO_PKG_VERSION` +/// 1. Reads version from `STAMPINF_VERSION` or `CARGO_PKG_VERSION` /// 2. Reads metadata from Cargo defaults and optional /// `[package.metadata.wdk.version-resource]` overrides /// 3. Generates a WDK-style `.rc` file in `OUT_DIR` @@ -538,203 +541,525 @@ fn compile_version_resource_inner(config: &Config) -> Result<(), ResourceCompile #[cfg(test)] mod tests { - use super::*; - use crate::CpuArchitecture; + use std::sync::Mutex; - // ── Version parsing tests ────────────────────────────────────────── + use assert_fs::prelude::*; - #[test] - fn parse_version_three_part() { - let v = parse_version("1.2.3").unwrap(); - assert_eq!( - v, - DriverVersion { - major: 1, - minor: 2, - patch: 3, - revision: 0 - } - ); - } + use super::*; + use crate::{ + CpuArchitecture, + utils::{remove_var, set_var}, + }; - #[test] - fn parse_version_four_part() { - let v = parse_version("1.2.3.4").unwrap(); - assert_eq!( - v, - DriverVersion { - major: 1, - minor: 2, - patch: 3, - revision: 4 - } - ); + struct EnvVarGuard { + original_values: Vec<(&'static str, Option)>, } - #[test] - fn parse_version_strips_prerelease() { - let v = parse_version("3.0.433-preview").unwrap(); - assert_eq!( - v, - DriverVersion { - major: 3, - minor: 0, - patch: 433, - revision: 0 + impl Drop for EnvVarGuard { + fn drop(&mut self) { + for (key, value) in &self.original_values { + match value { + Some(value) => set_var(key, value), + None => remove_var(key), + } } - ); + } } - #[test] - fn parse_version_max_values() { - let v = parse_version("65535.65535.65535.65535").unwrap(); - assert_eq!( - v, - DriverVersion { - major: 65535, - minor: 65535, - patch: 65535, - revision: 65535 + fn with_env(env_vars: &[(&'static str, Option<&str>)], f: F) -> R + where + F: FnOnce() -> R, + { + static ENV_MUTEX: Mutex<()> = Mutex::new(()); + + let _mutex_guard = ENV_MUTEX.lock().unwrap(); + let original_values = env_vars + .iter() + .map(|(key, _)| (*key, env::var(key).ok())) + .collect(); + let _env_var_guard = EnvVarGuard { original_values }; + + for (key, value) in env_vars { + match value { + Some(value) => set_var(key, value), + None => remove_var(key), } - ); - } + } - #[test] - fn parse_version_overflow_rejected() { - let result = parse_version("65536.0.0"); - assert!(result.is_err()); - let err = result.unwrap_err(); - assert!(matches!( - err, - ResourceCompileError::VersionParseError { .. } - )); + f() } - #[test] - fn parse_version_too_few_parts() { - assert!(parse_version("1.2").is_err()); + fn create_test_crate(cargo_toml: &str) -> assert_fs::TempDir { + let temp_dir = assert_fs::TempDir::new().unwrap(); + temp_dir.child("Cargo.toml").write_str(cargo_toml).unwrap(); + temp_dir.child("src").create_dir_all().unwrap(); + temp_dir.child("src").child("lib.rs").write_str("").unwrap(); + temp_dir } - #[test] - fn parse_version_too_many_parts() { - assert!(parse_version("1.2.3.4.5").is_err()); - } + mod version_parsing { + use super::*; + + #[test] + fn parse_version_three_part() { + let v = parse_version("1.2.3").unwrap(); + assert_eq!( + v, + DriverVersion { + major: 1, + minor: 2, + patch: 3, + revision: 0 + } + ); + } - #[test] - fn parse_version_empty_string() { - assert!(parse_version("").is_err()); - } + #[test] + fn parse_version_four_part() { + let v = parse_version("1.2.3.4").unwrap(); + assert_eq!( + v, + DriverVersion { + major: 1, + minor: 2, + patch: 3, + revision: 4 + } + ); + } - #[test] - fn parse_version_non_numeric() { - assert!(parse_version("1.2.abc").is_err()); - } + #[test] + fn parse_version_strips_prerelease() { + let v = parse_version("3.0.433-preview").unwrap(); + assert_eq!( + v, + DriverVersion { + major: 3, + minor: 0, + patch: 433, + revision: 0 + } + ); + } - #[test] - fn parse_version_zero() { - let v = parse_version("0.0.0").unwrap(); - assert_eq!( - v, - DriverVersion { - major: 0, - minor: 0, - patch: 0, - revision: 0 - } - ); - } + #[test] + fn parse_version_max_values() { + let v = parse_version("65535.65535.65535.65535").unwrap(); + assert_eq!( + v, + DriverVersion { + major: 65535, + minor: 65535, + patch: 65535, + revision: 65535 + } + ); + } + + #[test] + fn parse_version_overflow_rejected() { + let result = parse_version("65536.0.0"); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(matches!( + err, + ResourceCompileError::VersionParseError { .. } + )); + } + + #[test] + fn parse_version_too_few_parts() { + assert!(parse_version("1.2").is_err()); + } + + #[test] + fn parse_version_too_many_parts() { + assert!(parse_version("1.2.3.4.5").is_err()); + } - // ── DriverVersion formatting tests ──────────────────────────────── - - #[test] - fn windows_version_rc_numeric() { - let v = DriverVersion { - major: 1, - minor: 2, - patch: 3, - revision: 4, - }; - assert_eq!(v.as_rc_numeric(), "1,2,3,4"); + #[test] + fn parse_version_empty_string() { + assert!(parse_version("").is_err()); + } + + #[test] + fn parse_version_non_numeric() { + assert!(parse_version("1.2.abc").is_err()); + } + + #[test] + fn parse_version_zero() { + let v = parse_version("0.0.0").unwrap(); + assert_eq!( + v, + DriverVersion { + major: 0, + minor: 0, + patch: 0, + revision: 0 + } + ); + } } - #[test] - fn windows_version_rc_string() { - let v = DriverVersion { - major: 1, - minor: 2, - patch: 3, - revision: 4, - }; - assert_eq!(v.as_rc_string(), "1.2.3.4"); + mod version_resolution { + use super::*; + + #[test] + fn resolve_version_prefers_stampinf_version() { + let version = with_env( + &[ + (VERSION_ENV_VAR, Some("5.1.0")), + ("CARGO_PKG_VERSION", Some("1.2.3")), + ], + resolve_version, + ) + .unwrap(); + + assert_eq!( + version, + DriverVersion { + major: 5, + minor: 1, + patch: 0, + revision: 0 + } + ); + } + + #[test] + fn resolve_version_falls_back_to_cargo_pkg_version() { + let version = with_env( + &[ + (VERSION_ENV_VAR, None), + ("CARGO_PKG_VERSION", Some("1.2.3")), + ], + resolve_version, + ) + .unwrap(); + + assert_eq!( + version, + DriverVersion { + major: 1, + minor: 2, + patch: 3, + revision: 0 + } + ); + } + + #[test] + fn resolve_version_falls_back_to_cargo_pkg_version_when_stampinf_version_is_empty() { + let version = with_env( + &[ + (VERSION_ENV_VAR, Some("")), + ("CARGO_PKG_VERSION", Some("1.2.3")), + ], + resolve_version, + ) + .unwrap(); + + assert_eq!( + version, + DriverVersion { + major: 1, + minor: 2, + patch: 3, + revision: 0 + } + ); + } + + #[test] + fn resolve_version_errors_without_version_sources() { + let result = with_env( + &[(VERSION_ENV_VAR, None), ("CARGO_PKG_VERSION", None)], + resolve_version, + ); + + assert!(matches!( + result, + Err(ResourceCompileError::MetadataError { .. }) + )); + } } - // ── RC template generation tests ─────────────────────────────────── + mod driver_version_formatting { + use super::*; + + #[test] + fn windows_version_rc_numeric() { + let v = DriverVersion { + major: 1, + minor: 2, + patch: 3, + revision: 4, + }; + assert_eq!(v.as_rc_numeric(), "1,2,3,4"); + } - /// Create a test `Config` without relying on `Config::default()` which - /// requires build-script env vars like `CARGO_CFG_TARGET_ARCH`. - fn test_config(driver_config: DriverConfig) -> Config { - Config { - wdk_content_root: PathBuf::from("C:\\fake\\wdk"), - cpu_architecture: CpuArchitecture::Amd64, - driver_config, + #[test] + fn windows_version_rc_string() { + let v = DriverVersion { + major: 1, + minor: 2, + patch: 3, + revision: 4, + }; + assert_eq!(v.as_rc_string(), "1.2.3.4"); } } - #[test] - fn generate_rc_content_contains_expected_fields() { - let version = DriverVersion { - major: 1, - minor: 0, - patch: 0, - revision: 0, - }; - let metadata = VersionResourceMetadata { - company_name: "Test Corp".to_string(), - copyright: "Copyright Test".to_string(), - product_name: "Test Product".to_string(), - file_description: "Test Driver".to_string(), - internal_name: Some("test.sys".to_string()), - original_filename: Some("test.sys".to_string()), - }; - let config = test_config(DriverConfig::Kmdf(crate::KmdfConfig::default())); - - let rc = generate_rc_content(&version, &metadata, &config); - - assert!(rc.contains("#include ")); - assert!(rc.contains("#include ")); - assert!(rc.contains("#include \"common.ver\"")); - assert!(rc.contains("VFT_DRV")); - assert!(rc.contains("VFT2_DRV_SYSTEM")); - assert!(rc.contains("\"test.sys\"")); - assert!(rc.contains("\"Test Driver\"")); - assert!(rc.contains("\"Test Product\"")); - assert!(rc.contains("\"Test Corp\"")); - assert!(rc.contains("\"Copyright Test\"")); - assert!(rc.contains("1,0,0,0")); - assert!(rc.contains("\"1.0.0.0\"")); + mod metadata_resolution { + use super::*; + + #[test] + fn read_version_resource_metadata_uses_cargo_defaults_without_overrides() { + let temp_dir = create_test_crate( + r#" + [package] + name = "test-driver" + version = "1.2.3" + edition = "2021" + "#, + ); + let manifest_dir = temp_dir.path().to_string_lossy().to_string(); + + let metadata = with_env( + &[ + ("CARGO_MANIFEST_DIR", Some(&manifest_dir)), + ("CARGO_PKG_NAME", Some("test-driver")), + ("CARGO_PKG_AUTHORS", Some("Test Authors")), + ("CARGO_PKG_DESCRIPTION", Some("Default driver description")), + ], + read_version_resource_metadata, + ) + .unwrap(); + + assert_eq!(metadata.company_name, "Test Authors"); + assert_eq!(metadata.copyright, ""); + assert_eq!(metadata.product_name, "test-driver"); + assert_eq!(metadata.file_description, "Default driver description"); + assert_eq!(metadata.internal_name, None); + assert_eq!(metadata.original_filename, None); + } + + #[test] + fn read_version_resource_metadata_defaults_file_description_to_product_name() { + let temp_dir = create_test_crate( + r#" + [package] + name = "test-driver" + version = "1.2.3" + edition = "2021" + "#, + ); + let manifest_dir = temp_dir.path().to_string_lossy().to_string(); + + let metadata = with_env( + &[ + ("CARGO_MANIFEST_DIR", Some(&manifest_dir)), + ("CARGO_PKG_NAME", Some("test-driver")), + ("CARGO_PKG_AUTHORS", Some("")), + ("CARGO_PKG_DESCRIPTION", Some("")), + ], + read_version_resource_metadata, + ) + .unwrap(); + + assert_eq!(metadata.company_name, ""); + assert_eq!(metadata.product_name, "test-driver"); + assert_eq!(metadata.file_description, "test-driver"); + } + + #[test] + fn read_version_resource_metadata_applies_cargo_toml_overrides() { + let temp_dir = create_test_crate( + r#" + [package] + name = "test-driver" + version = "1.2.3" + edition = "2021" + + [package.metadata.wdk.version-resource] + company-name = "Override Company" + copyright = "Override Copyright" + product-name = "Override Product" + file-description = "Override Driver" + internal-name = "override.sys" + original-filename = "original.sys" + "#, + ); + let manifest_dir = temp_dir.path().to_string_lossy().to_string(); + + let metadata = with_env( + &[ + ("CARGO_MANIFEST_DIR", Some(&manifest_dir)), + ("CARGO_PKG_NAME", Some("test-driver")), + ("CARGO_PKG_AUTHORS", Some("Default Authors")), + ("CARGO_PKG_DESCRIPTION", Some("Default Description")), + ], + read_version_resource_metadata, + ) + .unwrap(); + + assert_eq!(metadata.company_name, "Override Company"); + assert_eq!(metadata.copyright, "Override Copyright"); + assert_eq!(metadata.product_name, "Override Product"); + assert_eq!(metadata.file_description, "Override Driver"); + assert_eq!(metadata.internal_name, Some("override.sys".to_string())); + assert_eq!(metadata.original_filename, Some("original.sys".to_string())); + } + + #[test] + fn read_version_resource_metadata_rejects_non_table_metadata() { + let temp_dir = create_test_crate( + r#" + [package] + name = "test-driver" + version = "1.2.3" + edition = "2021" + + [package.metadata.wdk] + version-resource = "invalid" + "#, + ); + let manifest_dir = temp_dir.path().to_string_lossy().to_string(); + + let result = with_env( + &[ + ("CARGO_MANIFEST_DIR", Some(&manifest_dir)), + ("CARGO_PKG_NAME", Some("test-driver")), + ], + read_version_resource_metadata, + ); + + assert!(matches!( + result, + Err(ResourceCompileError::MetadataError { detail }) + if detail.contains("[package.metadata.wdk.version-resource] must be a table") + )); + } + + #[test] + fn read_version_resource_metadata_rejects_non_string_fields() { + let temp_dir = create_test_crate( + r#" + [package] + name = "test-driver" + version = "1.2.3" + edition = "2021" + + [package.metadata.wdk.version-resource] + company-name = 42 + "#, + ); + let manifest_dir = temp_dir.path().to_string_lossy().to_string(); + + let result = with_env( + &[ + ("CARGO_MANIFEST_DIR", Some(&manifest_dir)), + ("CARGO_PKG_NAME", Some("test-driver")), + ], + read_version_resource_metadata, + ); + + assert!(matches!( + result, + Err(ResourceCompileError::MetadataError { detail }) + if detail.contains("[package.metadata.wdk.version-resource].company-name must be a string") + )); + } } - #[test] - fn generate_rc_content_umdf_uses_dll_type() { - let version = DriverVersion { - major: 1, - minor: 0, - patch: 0, - revision: 0, - }; - let metadata = VersionResourceMetadata { - company_name: "Test".to_string(), - copyright: "Copyright".to_string(), - product_name: "Product".to_string(), - file_description: "Driver".to_string(), - internal_name: None, - original_filename: None, - }; - let config = test_config(DriverConfig::Umdf(crate::UmdfConfig::default())); - - let rc = generate_rc_content(&version, &metadata, &config); - - assert!(rc.contains("VFT_DLL")); - assert!(rc.contains("VFT2_UNKNOWN")); + mod rc_generation { + use super::*; + + /// Create a test `Config` without relying on `Config::default()` which + /// requires build-script env vars like `CARGO_CFG_TARGET_ARCH`. + fn test_config(driver_config: DriverConfig) -> Config { + Config { + wdk_content_root: PathBuf::from("C:\\fake\\wdk"), + cpu_architecture: CpuArchitecture::Amd64, + driver_config, + } + } + + #[test] + fn resolve_driver_filename_uses_crate_artifact_name_and_driver_extension() { + let (kmdf_filename, umdf_filename) = + with_env(&[("CARGO_PKG_NAME", Some("surface-button"))], || { + ( + resolve_driver_filename(&test_config(DriverConfig::Kmdf( + crate::KmdfConfig::default(), + ))), + resolve_driver_filename(&test_config(DriverConfig::Umdf( + crate::UmdfConfig::default(), + ))), + ) + }); + + assert_eq!(kmdf_filename, "surface_button.sys"); + assert_eq!(umdf_filename, "surface_button.dll"); + } + + #[test] + fn generate_rc_content_contains_expected_fields() { + let version = DriverVersion { + major: 1, + minor: 0, + patch: 0, + revision: 0, + }; + let metadata = VersionResourceMetadata { + company_name: "Test Corp".to_string(), + copyright: "Copyright Test".to_string(), + product_name: "Test Product".to_string(), + file_description: "Test Driver".to_string(), + internal_name: Some("test.sys".to_string()), + original_filename: Some("test.sys".to_string()), + }; + let config = test_config(DriverConfig::Kmdf(crate::KmdfConfig::default())); + + let rc = generate_rc_content(&version, &metadata, &config); + + assert!(rc.contains("#include ")); + assert!(rc.contains("#include ")); + assert!(rc.contains("#include \"common.ver\"")); + assert!(rc.contains("VFT_DRV")); + assert!(rc.contains("VFT2_DRV_SYSTEM")); + assert!(rc.contains("\"test.sys\"")); + assert!(rc.contains("\"Test Driver\"")); + assert!(rc.contains("\"Test Product\"")); + assert!(rc.contains("\"Test Corp\"")); + assert!(rc.contains("\"Copyright Test\"")); + assert!(rc.contains("1,0,0,0")); + assert!(rc.contains("\"1.0.0.0\"")); + } + + #[test] + fn generate_rc_content_umdf_uses_dll_type() { + let version = DriverVersion { + major: 1, + minor: 0, + patch: 0, + revision: 0, + }; + let metadata = VersionResourceMetadata { + company_name: "Test".to_string(), + copyright: "Copyright".to_string(), + product_name: "Product".to_string(), + file_description: "Driver".to_string(), + internal_name: None, + original_filename: None, + }; + let config = test_config(DriverConfig::Umdf(crate::UmdfConfig::default())); + + let rc = generate_rc_content(&version, &metadata, &config); + + assert!(rc.contains("VFT_DLL")); + assert!(rc.contains("VFT2_UNKNOWN")); + } } } From c797b5c5f7a2287bdc87d27d61bab9c283ceee1b Mon Sep 17 00:00:00 2001 From: Alan Ngo Date: Fri, 22 May 2026 16:51:01 -0700 Subject: [PATCH 04/17] formatting --- crates/wdk-build/src/metadata/mod.rs | 13 ++++++------- crates/wdk-build/src/resource_compile.rs | 17 +++++++---------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/crates/wdk-build/src/metadata/mod.rs b/crates/wdk-build/src/metadata/mod.rs index 46323c799..5d0a14227 100644 --- a/crates/wdk-build/src/metadata/mod.rs +++ b/crates/wdk-build/src/metadata/mod.rs @@ -120,10 +120,9 @@ fn parse_packages_wdk_metadata( ) -> std::result::Result, TryFromCargoMetadataError> { let wdk_metadata_configurations = packages .iter() - .filter_map( - |package| match wdk_driver_model_metadata(&package.metadata["wdk"]) { - None => None, - Some(wdk_metadata) => Some(Wdk::deserialize(wdk_metadata).map_err(|err| { + .filter_map(|package| { + wdk_driver_model_metadata(&package.metadata["wdk"]).map(|wdk_metadata| { + Wdk::deserialize(wdk_metadata).map_err(|err| { TryFromCargoMetadataError::WdkMetadataDeserialization { metadata_source: format!( "{} for {} package", @@ -132,9 +131,9 @@ fn parse_packages_wdk_metadata( ), error_source: err, } - })), - }, - ) + }) + }) + }) .collect::, _>>()?; Ok(wdk_metadata_configurations) } diff --git a/crates/wdk-build/src/resource_compile.rs b/crates/wdk-build/src/resource_compile.rs index ee4987cea..08935071c 100644 --- a/crates/wdk-build/src/resource_compile.rs +++ b/crates/wdk-build/src/resource_compile.rs @@ -182,10 +182,7 @@ pub fn parse_version(version_str: &str) -> Result { return Err(ResourceCompileError::VersionParseError { value: version_str.to_string(), - reason: format!( - "expected 3 or 4 dot-separated components, found {}", - version_str.to_string() - ), + reason: format!("expected 3 or 4 dot-separated components, found {version_str}"), }); } }; @@ -289,12 +286,12 @@ fn read_version_resource_metadata() -> Result Date: Wed, 27 May 2026 11:47:45 -0700 Subject: [PATCH 05/17] fixes method for finding header paths for rc.exe calls and adds robustness for different dev environment setups --- crates/wdk-build/src/resource_compile.rs | 289 +++++++++++++++++++++-- 1 file changed, 265 insertions(+), 24 deletions(-) diff --git a/crates/wdk-build/src/resource_compile.rs b/crates/wdk-build/src/resource_compile.rs index 08935071c..0fde54609 100644 --- a/crates/wdk-build/src/resource_compile.rs +++ b/crates/wdk-build/src/resource_compile.rs @@ -125,7 +125,7 @@ pub enum ResourceCompileError { CompilerFailed { /// The exit status of `rc.exe` status: std::process::ExitStatus, - /// The stderr output from `rc.exe` + /// The stdout and stderr output from `rc.exe` stderr: String, }, @@ -339,6 +339,9 @@ fn version_resource_string( }) } +/// Reads an environment variable, returning `None` for both unset and empty +/// values. eWDK/vcvars occasionally export variables with empty defaults that +/// should be treated as unset. fn env_var_non_empty(key: &str) -> Option { env::var(key).ok().filter(|value| !value.is_empty()) } @@ -424,46 +427,147 @@ pub fn generate_rc_content( rc } -/// Compute the include paths needed for resource compilation. +/// Builds the `/I` include-path list passed to `rc.exe` when compiling the +/// driver's `VERSIONINFO` resource. /// -/// Unlike [`Config::include_paths`] (which provides paths for C/bindgen -/// compilation), resource compilation needs: -/// - `Include//um` (for `windows.h`) -/// - `Include//shared` (for shared headers) -/// - `Include//km` (for `ntverp.h`, kernel-mode drivers only) +/// Paths are collected from two roots so both combined WDK installs (SDK +/// headers nested under the WDK content root) and split eWDK layouts (SDK +/// headers in a separate Windows Kit referenced by `WindowsSdkDir` / +/// `WindowsSdkBinPath`) are supported: +/// +/// 1. SDK include root, if locatable from env vars: `um`, `shared`, `ucrt` +/// 2. WDK include root: `um`, `shared`, `ucrt` +/// 3. WDK kernel-mode headers (`km\crt`, `km`) for [`DriverConfig::Wdm`] and +/// [`DriverConfig::Kmdf`]; omitted for [`DriverConfig::Umdf`]. +/// +/// Missing subdirectories are silently skipped, and duplicates (e.g. when +/// the SDK and WDK roots coincide) are removed. +/// +/// # Errors +/// +/// Returns [`ResourceCompileError::MetadataError`] if no `um` directory is +/// found in either root (rc.exe would otherwise fail with a cryptic +/// `windows.h: No such file or directory`). fn resource_include_paths(config: &Config) -> Result, ResourceCompileError> { let sdk_version = crate::utils::detect_windows_sdk_version(&config.wdk_content_root) .map_err(|e| ResourceCompileError::ConfigError(Box::new(e)))?; - let include_directory = config.wdk_content_root.join("Include").join(&sdk_version); + let wdk_include_directory = config.wdk_content_root.join("Include").join(&sdk_version); let mut paths = vec![]; - // um directory contains windows.h - let um_path = include_directory.join("um"); - if um_path.is_dir() { - paths.push(absolute(&um_path)?); + if let Some(sdk_include_directory) = windows_sdk_include_directory(&sdk_version) { + add_resource_include_paths(&mut paths, &sdk_include_directory)?; } - // shared directory contains shared headers - let shared_path = include_directory.join("shared"); - if shared_path.is_dir() { - paths.push(absolute(&shared_path)?); - } + add_resource_include_paths(&mut paths, &wdk_include_directory)?; - // km directory contains ntverp.h (for kernel-mode drivers) match config.driver_config { DriverConfig::Wdm | DriverConfig::Kmdf(_) => { - let km_path = include_directory.join("km"); - if km_path.is_dir() { - paths.push(absolute(&km_path)?); - } + push_existing_absolute_path(&mut paths, &wdk_include_directory.join("km").join("crt"))?; + push_existing_absolute_path(&mut paths, &wdk_include_directory.join("km"))?; } DriverConfig::Umdf(_) => {} } + if !paths.iter().any(|p| p.ends_with("um")) { + return Err(ResourceCompileError::MetadataError { + detail: format!( + "Could not locate the Windows SDK 'um' include directory for SDK version \ + '{sdk_version}'. Looked under '{}' and the directories referenced by the \ + WindowsSdkDir / WindowsSdkBinPath environment variables. Build from an eWDK \ + developer prompt, or set WindowsSdkDir to a Windows Kit root that contains \ + 'Include\\{sdk_version}\\um'.", + wdk_include_directory.display(), + ), + }); + } + Ok(paths) } +/// Appends the standard user-mode include subdirectories (`um`, `shared`, +/// `ucrt`) of `include_directory` to `paths`. Subdirectories that do not +/// exist on disk are silently skipped. +fn add_resource_include_paths( + paths: &mut Vec, + include_directory: &Path, +) -> Result<(), ResourceCompileError> { + for include_subdirectory in ["um", "shared", "ucrt"] { + push_existing_absolute_path(paths, &include_directory.join(include_subdirectory))?; + } + + Ok(()) +} + +/// Pushes `path` onto `paths` as an absolute path, but only when it points to +/// an existing directory and is not already present. Uses +/// [`std::path::absolute`] (no filesystem walk) to avoid the `\\?\` UNC +/// prefixes that [`std::fs::canonicalize`] produces, which `rc.exe` mishandles. +fn push_existing_absolute_path( + paths: &mut Vec, + path: &Path, +) -> Result<(), ResourceCompileError> { + if path.is_dir() { + let absolute_path = absolute(path)?; + if !paths.contains(&absolute_path) { + paths.push(absolute_path); + } + } + + Ok(()) +} + +/// Resolves the Windows SDK include directory for `sdk_version` from +/// environment variables exported by the eWDK developer prompt / vcvars: +/// +/// 1. `WindowsSdkDir` — the Windows Kit root (preferred). +/// 2. `WindowsSdkBinPath` — a Kit `bin` subdirectory; the SDK root is found by +/// walking its ancestors. +/// +/// Returns `None` if neither variable points to a directory containing +/// `Include\`. +fn windows_sdk_include_directory(sdk_version: &str) -> Option { + env_var_non_empty("WindowsSdkDir") + .and_then(|windows_sdk_dir| { + include_directory_from_windows_sdk_root(Path::new(&windows_sdk_dir), sdk_version) + }) + .or_else(|| { + env_var_non_empty("WindowsSdkBinPath") + .and_then(|windows_sdk_bin_path| { + find_windows_sdk_root_from_bin_path( + Path::new(&windows_sdk_bin_path), + sdk_version, + ) + }) + .and_then(|windows_sdk_root| { + include_directory_from_windows_sdk_root(&windows_sdk_root, sdk_version) + }) + }) +} + +/// Returns `\Include\` if it exists, else +/// `None`. +fn include_directory_from_windows_sdk_root( + windows_sdk_root: &Path, + sdk_version: &str, +) -> Option { + let include_directory = windows_sdk_root.join("Include").join(sdk_version); + include_directory.is_dir().then_some(include_directory) +} + +/// Walks the ancestors of `windows_sdk_bin_path` (typically +/// `\bin\\`) and returns the first ancestor that +/// contains an `Include\` directory, i.e. the Windows Kit root. +fn find_windows_sdk_root_from_bin_path( + windows_sdk_bin_path: &Path, + sdk_version: &str, +) -> Option { + windows_sdk_bin_path + .ancestors() + .find(|path| include_directory_from_windows_sdk_root(path, sdk_version).is_some()) + .map(Path::to_path_buf) +} + /// Generate and compile a Windows `VERSIONINFO` resource, then emit a /// linker directive to embed it in the driver binary. /// @@ -524,9 +628,11 @@ fn compile_version_resource_inner(config: &Config) -> Result<(), ResourceCompile let output = cmd.output()?; if !output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); return Err(ResourceCompileError::CompilerFailed { status: output.status, - stderr: String::from_utf8_lossy(&output.stderr).to_string(), + stderr: format!("stdout:\n{stdout}\nstderr:\n{stderr}"), }); } @@ -538,7 +644,7 @@ fn compile_version_resource_inner(config: &Config) -> Result<(), ResourceCompile #[cfg(test)] mod tests { - use std::sync::Mutex; + use std::{fs, sync::Mutex}; use assert_fs::prelude::*; @@ -973,6 +1079,8 @@ mod tests { mod rc_generation { use super::*; + const SDK_VERSION: &str = "10.0.26100.0"; + /// Create a test `Config` without relying on `Config::default()` which /// requires build-script env vars like `CARGO_CFG_TARGET_ARCH`. fn test_config(driver_config: DriverConfig) -> Config { @@ -983,6 +1091,31 @@ mod tests { } } + fn test_config_with_wdk_root( + driver_config: DriverConfig, + wdk_content_root: PathBuf, + ) -> Config { + Config { + wdk_content_root, + cpu_architecture: CpuArchitecture::Amd64, + driver_config, + } + } + + fn create_include_subdirectories(include_directory: &Path, subdirectories: &[&str]) { + for subdirectory in subdirectories { + fs::create_dir_all(include_directory.join(subdirectory)).unwrap(); + } + } + + fn path_string(path: &Path) -> String { + path.to_string_lossy().to_string() + } + + fn absolute_paths(paths: &[PathBuf]) -> Vec { + paths.iter().map(|path| absolute(path).unwrap()).collect() + } + #[test] fn resolve_driver_filename_uses_crate_artifact_name_and_driver_extension() { let (kmdf_filename, umdf_filename) = @@ -1001,6 +1134,114 @@ mod tests { assert_eq!(umdf_filename, "surface_button.dll"); } + #[test] + fn resource_include_paths_support_split_sdk_and_wdk_layout_for_kmdf() { + let temp_dir = assert_fs::TempDir::new().unwrap(); + let sdk_root = temp_dir.path().join("sdk").join("c"); + let sdk_bin_path = sdk_root.join("bin"); + let sdk_include_directory = sdk_root.join("Include").join(SDK_VERSION); + let wdk_root = temp_dir.path().join("wdk").join("c"); + let wdk_include_directory = wdk_root.join("Include").join(SDK_VERSION); + + fs::create_dir_all(&sdk_bin_path).unwrap(); + create_include_subdirectories(&sdk_include_directory, &["um", "shared", "ucrt"]); + create_include_subdirectories(&wdk_include_directory, &["shared", "km\\crt", "km"]); + + let config = test_config_with_wdk_root( + DriverConfig::Kmdf(crate::KmdfConfig::default()), + wdk_root, + ); + let sdk_bin_path = path_string(&sdk_bin_path); + let include_paths = with_env( + &[ + ("Version_Number", Some(SDK_VERSION)), + ("WindowsSdkDir", None), + ("WindowsSdkBinPath", Some(&sdk_bin_path)), + ], + || resource_include_paths(&config), + ) + .unwrap(); + + assert_eq!( + include_paths, + absolute_paths(&[ + sdk_include_directory.join("um"), + sdk_include_directory.join("shared"), + sdk_include_directory.join("ucrt"), + wdk_include_directory.join("shared"), + wdk_include_directory.join("km").join("crt"), + wdk_include_directory.join("km"), + ]) + ); + } + + #[test] + fn resource_include_paths_omits_kernel_paths_for_umdf() { + let temp_dir = assert_fs::TempDir::new().unwrap(); + let sdk_root = temp_dir.path().join("sdk").join("c"); + let sdk_bin_path = sdk_root.join("bin"); + let sdk_include_directory = sdk_root.join("Include").join(SDK_VERSION); + let wdk_root = temp_dir.path().join("wdk").join("c"); + let wdk_include_directory = wdk_root.join("Include").join(SDK_VERSION); + + fs::create_dir_all(&sdk_bin_path).unwrap(); + create_include_subdirectories(&sdk_include_directory, &["um", "shared", "ucrt"]); + create_include_subdirectories(&wdk_include_directory, &["shared", "km\\crt", "km"]); + + let config = test_config_with_wdk_root( + DriverConfig::Umdf(crate::UmdfConfig::default()), + wdk_root, + ); + let sdk_bin_path = path_string(&sdk_bin_path); + let include_paths = with_env( + &[ + ("Version_Number", Some(SDK_VERSION)), + ("WindowsSdkDir", None), + ("WindowsSdkBinPath", Some(&sdk_bin_path)), + ], + || resource_include_paths(&config), + ) + .unwrap(); + + assert_eq!( + include_paths, + absolute_paths(&[ + sdk_include_directory.join("um"), + sdk_include_directory.join("shared"), + sdk_include_directory.join("ucrt"), + wdk_include_directory.join("shared"), + ]) + ); + } + + #[test] + fn resource_include_paths_errors_when_um_directory_missing() { + let temp_dir = assert_fs::TempDir::new().unwrap(); + let wdk_root = temp_dir.path().join("wdk").join("c"); + let wdk_include_directory = wdk_root.join("Include").join(SDK_VERSION); + create_include_subdirectories(&wdk_include_directory, &["shared", "km\\crt", "km"]); + + let config = test_config_with_wdk_root( + DriverConfig::Kmdf(crate::KmdfConfig::default()), + wdk_root, + ); + + let result = with_env( + &[ + ("Version_Number", Some(SDK_VERSION)), + ("WindowsSdkDir", None), + ("WindowsSdkBinPath", None), + ], + || resource_include_paths(&config), + ); + + assert!(matches!( + result, + Err(ResourceCompileError::MetadataError { detail }) + if detail.contains("'um'") && detail.contains("WindowsSdkDir") + )); + } + #[test] fn generate_rc_content_contains_expected_fields() { let version = DriverVersion { From 7bb32521051d081f7285218e7890476a044d44f1 Mon Sep 17 00:00:00 2001 From: Alan632 Date: Wed, 27 May 2026 13:59:42 -0700 Subject: [PATCH 06/17] change parse_version error information to include more info Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Alan632 --- crates/wdk-build/src/resource_compile.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/wdk-build/src/resource_compile.rs b/crates/wdk-build/src/resource_compile.rs index 0fde54609..3d96a6d8b 100644 --- a/crates/wdk-build/src/resource_compile.rs +++ b/crates/wdk-build/src/resource_compile.rs @@ -182,7 +182,11 @@ pub fn parse_version(version_str: &str) -> Result { return Err(ResourceCompileError::VersionParseError { value: version_str.to_string(), - reason: format!("expected 3 or 4 dot-separated components, found {version_str}"), + reason: format!( + "expected 3 or 4 dot-separated components, found {} in cleaned version `{}`", + parts.len(), + version_clean + ), }); } }; From 34613d425226de3175b723fe3583948c1add4076 Mon Sep 17 00:00:00 2001 From: Alan632 Date: Wed, 27 May 2026 14:28:12 -0700 Subject: [PATCH 07/17] remove resource_compile from wdk-build's public API Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Alan632 --- crates/wdk-build/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/wdk-build/src/lib.rs b/crates/wdk-build/src/lib.rs index 9c8619f2d..a0b38632f 100644 --- a/crates/wdk-build/src/lib.rs +++ b/crates/wdk-build/src/lib.rs @@ -24,7 +24,7 @@ use tracing::debug; pub mod cargo_make; pub mod metadata; -pub mod resource_compile; +mod resource_compile; mod utils; From 5ae23ca961fe613e2d5d0eb3f1461b9ca54fd755 Mon Sep 17 00:00:00 2001 From: Alan Ngo Date: Wed, 27 May 2026 15:03:33 -0700 Subject: [PATCH 08/17] modifies RC Error variant CompilerFailed, add short word exception to typos.toml used in rc.exe args --- .typos.toml | 5 +++++ crates/wdk-build/src/resource_compile.rs | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.typos.toml b/.typos.toml index 1133cb5f6..14959db27 100644 --- a/.typos.toml +++ b/.typos.toml @@ -1,6 +1,11 @@ [files] extend-exclude = ["**/Cargo.lock", "**/target/**"] +# Allowlist short words that collide with the typos dictionary but are +# legitimate in WDK/rc.exe contexts. +[default.extend-words] +fo = "fo" # rc.exe /fo (output file) switch + # Allow specific identifiers in code derived from WDK [default.extend-identifiers] STATUS_FVE_MOR_FAILED = "STATUS_FVE_MOR_FAILED" diff --git a/crates/wdk-build/src/resource_compile.rs b/crates/wdk-build/src/resource_compile.rs index 3d96a6d8b..ec7c10dd7 100644 --- a/crates/wdk-build/src/resource_compile.rs +++ b/crates/wdk-build/src/resource_compile.rs @@ -121,12 +121,12 @@ pub enum ResourceCompileError { }, /// The resource compiler (`rc.exe`) exited with a non-zero status. - #[error("rc.exe failed with {status}:\n{stderr}")] + #[error("rc.exe failed with {status}:\n{output}")] CompilerFailed { /// The exit status of `rc.exe` status: std::process::ExitStatus, /// The stdout and stderr output from `rc.exe` - stderr: String, + output: String, }, /// Metadata is missing or invalid. @@ -636,7 +636,7 @@ fn compile_version_resource_inner(config: &Config) -> Result<(), ResourceCompile let stderr = String::from_utf8_lossy(&output.stderr); return Err(ResourceCompileError::CompilerFailed { status: output.status, - stderr: format!("stdout:\n{stdout}\nstderr:\n{stderr}"), + output: format!("stdout:\n{stdout}\nstderr:\n{stderr}"), }); } From 6dc80b5bf30ebbcea771ed0bd26e7a051711bb05 Mon Sep 17 00:00:00 2001 From: Alan Ngo Date: Wed, 27 May 2026 15:38:08 -0700 Subject: [PATCH 09/17] fix public api surface (ResourceCompileError should be public, all else crate only), fix lint issues --- crates/wdk-build/src/lib.rs | 2 +- crates/wdk-build/src/resource_compile.rs | 43 +++++++++++------------- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/crates/wdk-build/src/lib.rs b/crates/wdk-build/src/lib.rs index a0b38632f..9c8619f2d 100644 --- a/crates/wdk-build/src/lib.rs +++ b/crates/wdk-build/src/lib.rs @@ -24,7 +24,7 @@ use tracing::debug; pub mod cargo_make; pub mod metadata; -mod resource_compile; +pub mod resource_compile; mod utils; diff --git a/crates/wdk-build/src/resource_compile.rs b/crates/wdk-build/src/resource_compile.rs index ec7c10dd7..7ab397762 100644 --- a/crates/wdk-build/src/resource_compile.rs +++ b/crates/wdk-build/src/resource_compile.rs @@ -55,23 +55,22 @@ const VERSION_ENV_VAR: &str = "STAMPINF_VERSION"; /// Windows `VERSIONINFO` resources use four 16-bit components: /// `MAJOR.MINOR.PATCH.REVISION`. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct DriverVersion { +pub(crate) struct DriverVersion { /// Major version number - pub major: u16, + pub(crate) major: u16, /// Minor version number - pub minor: u16, + pub(crate) minor: u16, /// Patch/build version number - pub patch: u16, + pub(crate) patch: u16, /// Revision number - pub revision: u16, + pub(crate) revision: u16, } impl DriverVersion { /// Format as a comma-separated string for the `VER_FILEVERSION` RC macro. /// /// Example: `1,2,3,0` - #[must_use] - pub fn as_rc_numeric(&self) -> String { + pub(crate) fn as_rc_numeric(self) -> String { format!( "{},{},{},{}", self.major, self.minor, self.patch, self.revision @@ -81,8 +80,7 @@ impl DriverVersion { /// Format as a dot-separated string for the `VER_FILEVERSION_STR` RC macro. /// /// Example: `"1.2.3.0"` - #[must_use] - pub fn as_rc_string(&self) -> String { + pub(crate) fn as_rc_string(self) -> String { format!( "{}.{}.{}.{}", self.major, self.minor, self.patch, self.revision @@ -93,19 +91,19 @@ impl DriverVersion { /// Metadata for the version resource, sourced from Cargo defaults and optional /// `[package.metadata.wdk.version-resource]` overrides in `Cargo.toml`. #[derive(Debug, Clone)] -pub struct VersionResourceMetadata { +pub(crate) struct VersionResourceMetadata { /// Company name (e.g. "Microsoft Corporation") - pub company_name: String, + pub(crate) company_name: String, /// Copyright string - pub copyright: String, + pub(crate) copyright: String, /// Product name (e.g. "Surface") - pub product_name: String, + pub(crate) product_name: String, /// File description shown in Explorer properties - pub file_description: String, + pub(crate) file_description: String, /// Internal name of the binary (e.g. "MyDriver.sys") - pub internal_name: Option, + pub(crate) internal_name: Option, /// Original filename of the binary - pub original_filename: Option, + pub(crate) original_filename: Option, } /// Errors specific to version resource compilation. @@ -158,7 +156,7 @@ pub enum ResourceCompileError { /// /// Returns [`ResourceCompileError::VersionParseError`] if the string cannot /// be parsed or any component exceeds the 16-bit limit. -pub fn parse_version(version_str: &str) -> Result { +fn parse_version(version_str: &str) -> Result { // Strip semver prerelease suffix (everything after first `-`) if present. // e.g. "3.0.433-preview" → "3.0.433" let version_clean = version_str.split('-').next().unwrap_or(version_str); @@ -360,9 +358,8 @@ fn env_var_non_empty(key: &str) -> Option { /// * `version` - The 4-part driver version to embed /// * `metadata` - Version resource metadata (company, product, etc.) /// * `config` - WDK build configuration (used to determine file type) -#[must_use] -pub fn generate_rc_content( - version: &DriverVersion, +fn generate_rc_content( + version: DriverVersion, metadata: &VersionResourceMetadata, config: &Config, ) -> String { @@ -614,7 +611,7 @@ fn compile_version_resource_inner(config: &Config) -> Result<(), ResourceCompile let res_path = out_dir.join("version.res"); // Generate RC file content - let rc_content = generate_rc_content(&version, &metadata, config); + let rc_content = generate_rc_content(version, &metadata, config); fs::write(&rc_path, &rc_content)?; // Invoke rc.exe (expected to be on PATH via eWDK prompt or cargo-wdk setup) @@ -1264,7 +1261,7 @@ mod tests { }; let config = test_config(DriverConfig::Kmdf(crate::KmdfConfig::default())); - let rc = generate_rc_content(&version, &metadata, &config); + let rc = generate_rc_content(version, &metadata, &config); assert!(rc.contains("#include ")); assert!(rc.contains("#include ")); @@ -1298,7 +1295,7 @@ mod tests { }; let config = test_config(DriverConfig::Umdf(crate::UmdfConfig::default())); - let rc = generate_rc_content(&version, &metadata, &config); + let rc = generate_rc_content(version, &metadata, &config); assert!(rc.contains("VFT_DLL")); assert!(rc.contains("VFT2_UNKNOWN")); From 4fdf8308d4b890c2967c55aa60b87998456bdee1 Mon Sep 17 00:00:00 2001 From: Alan Ngo Date: Wed, 27 May 2026 17:02:22 -0700 Subject: [PATCH 10/17] further minimize API surface to only what's necessary, remove unecessary rerun directive (CI builds are fresh), fix formatting in .typos.toml --- .typos.toml | 2 +- crates/wdk-build/src/lib.rs | 3 +- crates/wdk-build/src/resource_compile.rs | 42 ++++++++++++++---------- 3 files changed, 27 insertions(+), 20 deletions(-) diff --git a/.typos.toml b/.typos.toml index 14959db27..8f77526b3 100644 --- a/.typos.toml +++ b/.typos.toml @@ -4,7 +4,7 @@ extend-exclude = ["**/Cargo.lock", "**/target/**"] # Allowlist short words that collide with the typos dictionary but are # legitimate in WDK/rc.exe contexts. [default.extend-words] -fo = "fo" # rc.exe /fo (output file) switch +fo = "fo" # rc.exe /fo (output file) switch # Allow specific identifiers in code derived from WDK [default.extend-identifiers] diff --git a/crates/wdk-build/src/lib.rs b/crates/wdk-build/src/lib.rs index 9c8619f2d..57805fac8 100644 --- a/crates/wdk-build/src/lib.rs +++ b/crates/wdk-build/src/lib.rs @@ -20,12 +20,13 @@ use std::{ pub use bindgen::BuilderExt; use metadata::TryFromCargoMetadataError; +pub use resource_compile::ResourceCompileError; use tracing::debug; pub mod cargo_make; pub mod metadata; -pub mod resource_compile; +mod resource_compile; mod utils; mod bindgen; diff --git a/crates/wdk-build/src/resource_compile.rs b/crates/wdk-build/src/resource_compile.rs index 7ab397762..266fd2962 100644 --- a/crates/wdk-build/src/resource_compile.rs +++ b/crates/wdk-build/src/resource_compile.rs @@ -55,22 +55,22 @@ const VERSION_ENV_VAR: &str = "STAMPINF_VERSION"; /// Windows `VERSIONINFO` resources use four 16-bit components: /// `MAJOR.MINOR.PATCH.REVISION`. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) struct DriverVersion { +struct DriverVersion { /// Major version number - pub(crate) major: u16, + major: u16, /// Minor version number - pub(crate) minor: u16, + minor: u16, /// Patch/build version number - pub(crate) patch: u16, + patch: u16, /// Revision number - pub(crate) revision: u16, + revision: u16, } impl DriverVersion { /// Format as a comma-separated string for the `VER_FILEVERSION` RC macro. /// /// Example: `1,2,3,0` - pub(crate) fn as_rc_numeric(self) -> String { + fn as_rc_numeric(self) -> String { format!( "{},{},{},{}", self.major, self.minor, self.patch, self.revision @@ -80,7 +80,7 @@ impl DriverVersion { /// Format as a dot-separated string for the `VER_FILEVERSION_STR` RC macro. /// /// Example: `"1.2.3.0"` - pub(crate) fn as_rc_string(self) -> String { + fn as_rc_string(self) -> String { format!( "{}.{}.{}.{}", self.major, self.minor, self.patch, self.revision @@ -91,19 +91,19 @@ impl DriverVersion { /// Metadata for the version resource, sourced from Cargo defaults and optional /// `[package.metadata.wdk.version-resource]` overrides in `Cargo.toml`. #[derive(Debug, Clone)] -pub(crate) struct VersionResourceMetadata { +struct VersionResourceMetadata { /// Company name (e.g. "Microsoft Corporation") - pub(crate) company_name: String, + company_name: String, /// Copyright string - pub(crate) copyright: String, + copyright: String, /// Product name (e.g. "Surface") - pub(crate) product_name: String, + product_name: String, /// File description shown in Explorer properties - pub(crate) file_description: String, + file_description: String, /// Internal name of the binary (e.g. "MyDriver.sys") - pub(crate) internal_name: Option, + internal_name: Option, /// Original filename of the binary - pub(crate) original_filename: Option, + original_filename: Option, } /// Errors specific to version resource compilation. @@ -590,15 +590,21 @@ fn find_windows_sdk_root_from_bin_path( /// # Panics /// /// Panics if `OUT_DIR` is not set (i.e., not called from a build script). -pub(crate) fn compile_version_resource(config: &Config) -> Result<(), ConfigError> { +// `pub(super)` is the minimum visibility needed for `lib.rs` to invoke this +// from `Config::configure_binary_build`. Clippy's `redundant_pub_crate` flags +// it as redundant because the module is private, but removing the visibility +// breaks the call site (E0603), so the lint is a false positive here. +#[allow( + clippy::redundant_pub_crate, + reason = "Needed for call from Config::configure_binary_build in lib.rs, flagged because \ + module is private." +)] +pub(super) fn compile_version_resource(config: &Config) -> Result<(), ConfigError> { Ok(compile_version_resource_inner(config)?) } /// Inner implementation that uses [`ResourceCompileError`] directly. fn compile_version_resource_inner(config: &Config) -> Result<(), ResourceCompileError> { - // Emit rerun-if-env-changed for version-affecting variables - println!("cargo::rerun-if-env-changed={VERSION_ENV_VAR}"); - let version = resolve_version()?; let metadata = read_version_resource_metadata()?; From 6ed4a9d29170aac7b831b090712ac5d67933b007 Mon Sep 17 00:00:00 2001 From: Alan Ngo Date: Thu, 28 May 2026 11:18:12 -0700 Subject: [PATCH 11/17] add validation of resource metadata values along with unit tests --- crates/wdk-build/src/resource_compile.rs | 174 +++++++++++++++++++++++ 1 file changed, 174 insertions(+) diff --git a/crates/wdk-build/src/resource_compile.rs b/crates/wdk-build/src/resource_compile.rs index 266fd2962..831b2c599 100644 --- a/crates/wdk-build/src/resource_compile.rs +++ b/crates/wdk-build/src/resource_compile.rs @@ -314,6 +314,18 @@ fn read_version_resource_metadata() -> Result Result<(), ResourceCompileError> { + for (offset, ch) in value.char_indices() { + let reason = if ch == '"' { + Some("contains a double-quote ('\"') which would terminate the RC string literal") + } else if ch == '\\' { + Some( + "contains a backslash ('\\') which could alter RC escape sequences; use ASCII \ + forward slashes or omit", + ) + } else if ch.is_control() { + Some( + "contains a control character which cannot be safely embedded in an RC string \ + literal", + ) + } else { + None + }; + if let Some(reason) = reason { + return Err(ResourceCompileError::MetadataError { + detail: format!( + "version-resource field '{field}' {reason} (offending character U+{:04X} at \ + byte offset {offset})", + ch as u32, + ), + }); + } + } + Ok(()) +} + /// Reads an environment variable, returning `None` for both unset and empty /// values. eWDK/vcvars occasionally export variables with empty defaults that /// should be treated as unset. @@ -1081,6 +1137,124 @@ mod tests { if detail.contains("[package.metadata.wdk.version-resource].company-name must be a string") )); } + + #[test] + fn read_version_resource_metadata_rejects_double_quote_injection() { + let temp_dir = create_test_crate( + r#" + [package] + name = "test-driver" + version = "1.2.3" + edition = "2021" + + [package.metadata.wdk.version-resource] + company-name = '"; #include "evil.rc"; "' + "#, + ); + let manifest_dir = temp_dir.path().to_string_lossy().to_string(); + + let result = with_env( + &[ + ("CARGO_MANIFEST_DIR", Some(&manifest_dir)), + ("CARGO_PKG_NAME", Some("test-driver")), + ], + read_version_resource_metadata, + ); + + assert!(matches!( + result, + Err(ResourceCompileError::MetadataError { detail }) + if detail.contains("company-name") && detail.contains("double-quote") + )); + } + + #[test] + fn read_version_resource_metadata_rejects_control_character() { + let temp_dir = create_test_crate( + r#" + [package] + name = "test-driver" + version = "1.2.3" + edition = "2021" + + [package.metadata.wdk.version-resource] + copyright = "line1\nline2" + "#, + ); + let manifest_dir = temp_dir.path().to_string_lossy().to_string(); + + let result = with_env( + &[ + ("CARGO_MANIFEST_DIR", Some(&manifest_dir)), + ("CARGO_PKG_NAME", Some("test-driver")), + ], + read_version_resource_metadata, + ); + + assert!(matches!( + result, + Err(ResourceCompileError::MetadataError { detail }) + if detail.contains("copyright") && detail.contains("control character") + )); + } + + #[test] + fn read_version_resource_metadata_rejects_backslash() { + let temp_dir = create_test_crate( + r#" + [package] + name = "test-driver" + version = "1.2.3" + edition = "2021" + + [package.metadata.wdk.version-resource] + file-description = "Driver for C:\\Windows" + "#, + ); + let manifest_dir = temp_dir.path().to_string_lossy().to_string(); + + let result = with_env( + &[ + ("CARGO_MANIFEST_DIR", Some(&manifest_dir)), + ("CARGO_PKG_NAME", Some("test-driver")), + ], + read_version_resource_metadata, + ); + + assert!(matches!( + result, + Err(ResourceCompileError::MetadataError { detail }) + if detail.contains("file-description") && detail.contains("backslash") + )); + } + + #[test] + fn read_version_resource_metadata_validates_env_var_fallback() { + let temp_dir = create_test_crate( + r#" + [package] + name = "test-driver" + version = "1.2.3" + edition = "2021" + "#, + ); + let manifest_dir = temp_dir.path().to_string_lossy().to_string(); + + let result = with_env( + &[ + ("CARGO_MANIFEST_DIR", Some(&manifest_dir)), + ("CARGO_PKG_NAME", Some("test-driver")), + ("CARGO_PKG_DESCRIPTION", Some("Driver with \" injection")), + ], + read_version_resource_metadata, + ); + + assert!(matches!( + result, + Err(ResourceCompileError::MetadataError { detail }) + if detail.contains("file-description") && detail.contains("double-quote") + )); + } } mod rc_generation { From 6790cdeb364a708c8dfd8d6e7daca091dd8e9e9a Mon Sep 17 00:00:00 2001 From: Alan Ngo Date: Thu, 28 May 2026 15:36:23 -0700 Subject: [PATCH 12/17] add stripping build info from version and new unit tests, improve ResourceCompileError by changing variant to use path aware IoError --- crates/wdk-build/src/resource_compile.rs | 61 +++++++++++++++++++----- 1 file changed, 48 insertions(+), 13 deletions(-) diff --git a/crates/wdk-build/src/resource_compile.rs b/crates/wdk-build/src/resource_compile.rs index 831b2c599..2edee93dd 100644 --- a/crates/wdk-build/src/resource_compile.rs +++ b/crates/wdk-build/src/resource_compile.rs @@ -30,8 +30,9 @@ //! 2. `CARGO_PKG_VERSION` (from `Cargo.toml` `[package]` version) //! //! Semver versions are mapped to 4-part Windows versions by appending `.0` -//! for the revision component. Prerelease suffixes (e.g. `-preview`) are -//! stripped. Each component must fit in a 16-bit word (0–65535). +//! for the revision component. Prerelease suffixes (e.g. `-preview`) and +//! build metadata suffixes (e.g. `+ci.42`) are stripped. Each component must +//! fit in a 16-bit word (0–65535). use std::{ env, @@ -41,13 +42,14 @@ use std::{ process::Command, }; -use crate::{Config, ConfigError, DriverConfig}; +use crate::{Config, ConfigError, DriverConfig, IoError}; /// Environment variable for overriding the driver version in CI pipelines. /// /// When set, this takes priority over `CARGO_PKG_VERSION`. The value should /// be in the format `MAJOR.MINOR.PATCH` or `MAJOR.MINOR.PATCH.REVISION`. -/// A prerelease suffix (e.g. `-preview`) is stripped automatically. +/// A prerelease suffix (e.g. `-preview`) or build metadata suffix +/// (e.g. `+ci.42`) is stripped automatically. const VERSION_ENV_VAR: &str = "STAMPINF_VERSION"; /// A parsed 4-part Windows version number. @@ -134,9 +136,10 @@ pub enum ResourceCompileError { detail: String, }, - /// An I/O error occurred during resource compilation. - #[error("I/O error during resource compilation")] - IoError(#[from] std::io::Error), + /// An I/O error (with path context) that occurred during resource + /// compilation. + #[error(transparent)] + IoError(#[from] IoError), /// An error from the WDK build configuration. #[error("WDK build configuration error during resource compilation")] @@ -149,6 +152,7 @@ pub enum ResourceCompileError { /// - `MAJOR.MINOR.PATCH` (revision defaults to 0) /// - `MAJOR.MINOR.PATCH.REVISION` /// - Semver with prerelease tag: `1.2.3-alpha` (prerelease suffix is stripped) +/// - Semver with build metadata: `1.2.3+ci.42` (build metadata is stripped) /// /// Each component must be in the range `0..=65535`. /// @@ -157,9 +161,10 @@ pub enum ResourceCompileError { /// Returns [`ResourceCompileError::VersionParseError`] if the string cannot /// be parsed or any component exceeds the 16-bit limit. fn parse_version(version_str: &str) -> Result { - // Strip semver prerelease suffix (everything after first `-`) if present. - // e.g. "3.0.433-preview" → "3.0.433" - let version_clean = version_str.split('-').next().unwrap_or(version_str); + // Strip semver prerelease suffix (after first `-`) and/or build metadata + // suffix (after first `+`) if present. + // e.g. "3.0.433-preview" → "3.0.433", "1.2.3+ci.42" → "1.2.3" + let version_clean = version_str.split(['-', '+']).next().unwrap_or(version_str); let parts: Vec<&str> = version_clean.split('.').collect(); @@ -565,7 +570,7 @@ fn push_existing_absolute_path( path: &Path, ) -> Result<(), ResourceCompileError> { if path.is_dir() { - let absolute_path = absolute(path)?; + let absolute_path = absolute(path).map_err(|source| IoError::with_path(path, source))?; if !paths.contains(&absolute_path) { paths.push(absolute_path); } @@ -674,7 +679,7 @@ fn compile_version_resource_inner(config: &Config) -> Result<(), ResourceCompile // Generate RC file content let rc_content = generate_rc_content(version, &metadata, config); - fs::write(&rc_path, &rc_content)?; + fs::write(&rc_path, &rc_content).map_err(|source| IoError::with_path(&rc_path, source))?; // Invoke rc.exe (expected to be on PATH via eWDK prompt or cargo-wdk setup) let include_paths = resource_include_paths(config)?; @@ -688,7 +693,9 @@ fn compile_version_resource_inner(config: &Config) -> Result<(), ResourceCompile cmd.arg(&res_path); cmd.arg(&rc_path); - let output = cmd.output()?; + let output = cmd + .output() + .map_err(|source| IoError::with_path("rc.exe", source))?; if !output.status.success() { let stdout = String::from_utf8_lossy(&output.stdout); @@ -808,6 +815,34 @@ mod tests { ); } + #[test] + fn parse_version_strips_build_metadata() { + let v = parse_version("1.2.3+ci.42").unwrap(); + assert_eq!( + v, + DriverVersion { + major: 1, + minor: 2, + patch: 3, + revision: 0 + } + ); + } + + #[test] + fn parse_version_strips_prerelease_and_build_metadata() { + let v = parse_version("1.2.3-rc.1+git.abc1234").unwrap(); + assert_eq!( + v, + DriverVersion { + major: 1, + minor: 2, + patch: 3, + revision: 0 + } + ); + } + #[test] fn parse_version_max_values() { let v = parse_version("65535.65535.65535.65535").unwrap(); From cfe4d0b13c7a448179a14cded751aab6a18f3598 Mon Sep 17 00:00:00 2001 From: Alan Ngo Date: Thu, 11 Jun 2026 16:21:08 -0700 Subject: [PATCH 13/17] add linker arg to prevent .rsrc from being discarded --- crates/wdk-build/src/resource_compile.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/wdk-build/src/resource_compile.rs b/crates/wdk-build/src/resource_compile.rs index 2edee93dd..b2aa2ec2d 100644 --- a/crates/wdk-build/src/resource_compile.rs +++ b/crates/wdk-build/src/resource_compile.rs @@ -709,6 +709,10 @@ fn compile_version_resource_inner(config: &Config) -> Result<(), ResourceCompile // Emit linker directive to include the compiled resource println!("cargo::rustc-cdylib-link-arg={}", res_path.display()); + // Mark the .rsrc section as non-discardable so the embedded version + // resource is not paged out after driver initialization. + println!("cargo::rustc-cdylib-link-arg=/section:.rsrc,!d"); + Ok(()) } From badcee0badfe4860f42d2262402f736fcd5dc300 Mon Sep 17 00:00:00 2001 From: Alan Ngo Date: Mon, 22 Jun 2026 17:12:48 -0700 Subject: [PATCH 14/17] - constrain code page to UTF-8 (inputs into rc.exe) - add pass through of source error into ConfigError's output --- crates/wdk-build/src/resource_compile.rs | 52 +++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/crates/wdk-build/src/resource_compile.rs b/crates/wdk-build/src/resource_compile.rs index b2aa2ec2d..1d3f6afc6 100644 --- a/crates/wdk-build/src/resource_compile.rs +++ b/crates/wdk-build/src/resource_compile.rs @@ -7,6 +7,7 @@ //! file) that gets linked into the driver binary. This embeds version metadata //! (file version, product name, company, copyright, etc.) into the `.sys` or //! `.dll` PE file, making it visible in Windows Explorer's file properties. +//! The generated `.rc` declares UTF-8 with `#pragma code_page(65001)`. //! //! # Usage //! @@ -142,7 +143,7 @@ pub enum ResourceCompileError { IoError(#[from] IoError), /// An error from the WDK build configuration. - #[error("WDK build configuration error during resource compilation")] + #[error("WDK build configuration error during resource compilation: {0}")] ConfigError(#[source] Box), } @@ -441,6 +442,7 @@ fn generate_rc_content( }; let mut rc = String::with_capacity(1024); + writeln!(rc, "#pragma code_page(65001)").expect("write to String should not fail"); writeln!(rc, "#include ").expect("write to String should not fail"); writeln!(rc, "#include ").expect("write to String should not fail"); writeln!(rc).expect("write to String should not fail"); @@ -1462,6 +1464,29 @@ mod tests { )); } + #[test] + fn generate_rc_content_declares_utf8_code_page_first() { + let version = DriverVersion { + major: 1, + minor: 0, + patch: 0, + revision: 0, + }; + let metadata = VersionResourceMetadata { + company_name: "Test Corp".to_string(), + copyright: "Copyright Test".to_string(), + product_name: "Test Product".to_string(), + file_description: "Test Driver".to_string(), + internal_name: Some("test.sys".to_string()), + original_filename: Some("test.sys".to_string()), + }; + let config = test_config(DriverConfig::Kmdf(crate::KmdfConfig::default())); + + let rc = generate_rc_content(version, &metadata, &config); + + assert!(rc.starts_with("#pragma code_page(65001)\n")); + } + #[test] fn generate_rc_content_contains_expected_fields() { let version = DriverVersion { @@ -1482,6 +1507,7 @@ mod tests { let rc = generate_rc_content(version, &metadata, &config); + assert!(rc.contains("#pragma code_page(65001)")); assert!(rc.contains("#include ")); assert!(rc.contains("#include ")); assert!(rc.contains("#include \"common.ver\"")); @@ -1519,5 +1545,29 @@ mod tests { assert!(rc.contains("VFT_DLL")); assert!(rc.contains("VFT2_UNKNOWN")); } + + #[test] + fn generate_rc_content_preserves_non_ascii_metadata() { + let version = DriverVersion { + major: 1, + minor: 0, + patch: 0, + revision: 0, + }; + let metadata = VersionResourceMetadata { + company_name: "Tëst Çörp".to_string(), + copyright: "Copyright".to_string(), + product_name: "Prödüct".to_string(), + file_description: "Driver".to_string(), + internal_name: Some("test.sys".to_string()), + original_filename: Some("test.sys".to_string()), + }; + let config = test_config(DriverConfig::Kmdf(crate::KmdfConfig::default())); + + let rc = generate_rc_content(version, &metadata, &config); + + assert!(rc.contains("\"Tëst Çörp\"")); + assert!(rc.contains("\"Prödüct\"")); + } } } From 21202ee6b834ffb513452ac643fd1ded6dbaea20 Mon Sep 17 00:00:00 2001 From: Alan632 Date: Mon, 22 Jun 2026 17:45:09 -0700 Subject: [PATCH 15/17] fixing comment Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Alan632 --- crates/wdk-build/src/resource_compile.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/wdk-build/src/resource_compile.rs b/crates/wdk-build/src/resource_compile.rs index 1d3f6afc6..8965ede73 100644 --- a/crates/wdk-build/src/resource_compile.rs +++ b/crates/wdk-build/src/resource_compile.rs @@ -712,7 +712,7 @@ fn compile_version_resource_inner(config: &Config) -> Result<(), ResourceCompile println!("cargo::rustc-cdylib-link-arg={}", res_path.display()); // Mark the .rsrc section as non-discardable so the embedded version - // resource is not paged out after driver initialization. + // resource is not discarded after driver initialization. println!("cargo::rustc-cdylib-link-arg=/section:.rsrc,!d"); Ok(()) From 2833f39639e95193efe4630aba906f603e0fb28b Mon Sep 17 00:00:00 2001 From: Alan Ngo Date: Tue, 23 Jun 2026 17:10:23 -0700 Subject: [PATCH 16/17] - refactor to utilize deserialization to build VersionResourceOverrides - add a handler to deal with multiple authors cleanly - move env_var_non_empty to utils.rs - fix a cyclic error types with ResourceCompileError --- crates/wdk-build/src/resource_compile.rs | 168 ++++++++++++++++------- crates/wdk-build/src/utils.rs | 7 + 2 files changed, 127 insertions(+), 48 deletions(-) diff --git a/crates/wdk-build/src/resource_compile.rs b/crates/wdk-build/src/resource_compile.rs index 8965ede73..41f68fba3 100644 --- a/crates/wdk-build/src/resource_compile.rs +++ b/crates/wdk-build/src/resource_compile.rs @@ -43,7 +43,13 @@ use std::{ process::Command, }; -use crate::{Config, ConfigError, DriverConfig, IoError}; +use crate::{ + Config, + ConfigError, + DriverConfig, + IoError, + utils::{detect_windows_sdk_version, env_var_non_empty}, +}; /// Environment variable for overriding the driver version in CI pipelines. /// @@ -91,6 +97,27 @@ impl DriverVersion { } } +/// Optional overrides from `[package.metadata.wdk.version-resource]` in +/// `Cargo.toml`. +/// +/// All fields are optional; absent keys fall back to Cargo-derived defaults. +#[derive(Debug, Clone, Default, serde::Deserialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +struct VersionResourceOverrides { + /// Company name override + company_name: Option, + /// Copyright string override + copyright: Option, + /// Product name override + product_name: Option, + /// File description override + file_description: Option, + /// Internal name of the binary (e.g. "MyDriver.sys") + internal_name: Option, + /// Original filename of the binary + original_filename: Option, +} + /// Metadata for the version resource, sourced from Cargo defaults and optional /// `[package.metadata.wdk.version-resource]` overrides in `Cargo.toml`. #[derive(Debug, Clone)] @@ -142,9 +169,10 @@ pub enum ResourceCompileError { #[error(transparent)] IoError(#[from] IoError), - /// An error from the WDK build configuration. - #[error("WDK build configuration error during resource compilation: {0}")] - ConfigError(#[source] Box), + /// The Windows SDK version needed to locate `rc.exe` include paths could + /// not be detected from the WDK content root. + #[error("failed to detect the Windows SDK version for resource compilation: {0}")] + SdkVersionDetection(String), } /// Parse a version string into a [`DriverVersion`]. @@ -290,35 +318,43 @@ fn read_version_resource_metadata() -> Result Result, - key: &str, -) -> Result, ResourceCompileError> { - let Some(value) = version_resource.and_then(|metadata| metadata.get(key)) else { - return Ok(None); - }; - - value - .as_str() - .map(ToString::to_string) - .map(Some) - .ok_or_else(|| ResourceCompileError::MetadataError { - detail: format!("[package.metadata.wdk.version-resource].{key} must be a string"), - }) +/// Normalize the `CARGO_PKG_AUTHORS` value into a readable company name. +/// +/// Cargo joins multiple `authors` entries with a colon (`:`), so the raw value +/// for `["Alice", "Bob"]` is `"Alice:Bob"`. Embedding that verbatim into the +/// `CompanyName` resource string shows a colon-separated blob in Explorer. +/// This renders the authors as a comma-separated list instead, trimming each +/// entry and dropping empty segments. +fn format_cargo_authors(authors: &str) -> String { + authors + .split(':') + .map(str::trim) + .filter(|author| !author.is_empty()) + .collect::>() + .join(", ") } /// Validates that a metadata string is safe to interpolate into an RC string @@ -403,13 +438,6 @@ fn validate_rc_metadata_string(field: &str, value: &str) -> Result<(), ResourceC Ok(()) } -/// Reads an environment variable, returning `None` for both unset and empty -/// values. eWDK/vcvars occasionally export variables with empty defaults that -/// should be treated as unset. -fn env_var_non_empty(key: &str) -> Option { - env::var(key).ok().filter(|value| !value.is_empty()) -} - /// Generate the contents of a WDK-style `.rc` file. /// /// The generated file uses the standard Windows driver pattern: @@ -509,12 +537,14 @@ fn generate_rc_content( /// /// # Errors /// -/// Returns [`ResourceCompileError::MetadataError`] if no `um` directory is -/// found in either root (rc.exe would otherwise fail with a cryptic +/// Returns [`ResourceCompileError::SdkVersionDetection`] if the Windows SDK +/// version cannot be detected from the WDK content root, or +/// [`ResourceCompileError::MetadataError`] if no `um` directory is found in +/// either root (rc.exe would otherwise fail with a cryptic /// `windows.h: No such file or directory`). fn resource_include_paths(config: &Config) -> Result, ResourceCompileError> { - let sdk_version = crate::utils::detect_windows_sdk_version(&config.wdk_content_root) - .map_err(|e| ResourceCompileError::ConfigError(Box::new(e)))?; + let sdk_version = detect_windows_sdk_version(&config.wdk_content_root) + .map_err(|e| ResourceCompileError::SdkVersionDetection(e.to_string()))?; let wdk_include_directory = config.wdk_content_root.join("Include").join(&sdk_version); let mut paths = vec![]; @@ -1021,6 +1051,48 @@ mod tests { mod metadata_resolution { use super::*; + #[test] + fn format_cargo_authors_renders_multiple_authors_as_comma_list() { + assert_eq!(format_cargo_authors("Solo Author"), "Solo Author"); + assert_eq!( + format_cargo_authors("Alice :Bob "), + "Alice , Bob " + ); + // Extra whitespace and empty segments are trimmed/dropped. + assert_eq!(format_cargo_authors("Alice : : Bob"), "Alice, Bob"); + } + + #[test] + fn read_version_resource_metadata_joins_multiple_authors_for_company_name() { + let temp_dir = create_test_crate( + r#" + [package] + name = "test-driver" + version = "1.2.3" + edition = "2021" + "#, + ); + let manifest_dir = temp_dir.path().to_string_lossy().to_string(); + + let metadata = with_env( + &[ + ("CARGO_MANIFEST_DIR", Some(&manifest_dir)), + ("CARGO_PKG_NAME", Some("test-driver")), + ( + "CARGO_PKG_AUTHORS", + Some("Alice :Bob "), + ), + ], + read_version_resource_metadata, + ) + .unwrap(); + + assert_eq!( + metadata.company_name, + "Alice , Bob " + ); + } + #[test] fn read_version_resource_metadata_uses_cargo_defaults_without_overrides() { let temp_dir = create_test_crate( @@ -1175,7 +1247,7 @@ mod tests { assert!(matches!( result, Err(ResourceCompileError::MetadataError { detail }) - if detail.contains("[package.metadata.wdk.version-resource].company-name must be a string") + if detail.contains("[package.metadata.wdk.version-resource] is invalid") )); } diff --git a/crates/wdk-build/src/utils.rs b/crates/wdk-build/src/utils.rs index 43fa7716a..c964f97c5 100644 --- a/crates/wdk-build/src/utils.rs +++ b/crates/wdk-build/src/utils.rs @@ -420,6 +420,13 @@ where ); } +/// Reads an environment variable, returning `None` for both unset and empty +/// values. eWDK/vcvars occasionally export variables with empty defaults that +/// should be treated as unset. +pub fn env_var_non_empty(key: &str) -> Option { + env::var(key).ok().filter(|value| !value.is_empty()) +} + #[cfg(test)] mod tests { use assert_fs::prelude::*; From d3157e730be9ea2f5ceb9d8c985a13777ae443e7 Mon Sep 17 00:00:00 2001 From: Alan Ngo Date: Mon, 29 Jun 2026 10:53:34 -0700 Subject: [PATCH 17/17] - update include syntax for common.ver in resource compilation --- crates/wdk-build/src/resource_compile.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/wdk-build/src/resource_compile.rs b/crates/wdk-build/src/resource_compile.rs index 41f68fba3..cf4bb8127 100644 --- a/crates/wdk-build/src/resource_compile.rs +++ b/crates/wdk-build/src/resource_compile.rs @@ -514,7 +514,7 @@ fn generate_rc_content( writeln!(rc).expect("write to String should not fail"); } - writeln!(rc, "#include \"common.ver\"").expect("write to String should not fail"); + writeln!(rc, "#include ").expect("write to String should not fail"); rc } @@ -1582,7 +1582,7 @@ mod tests { assert!(rc.contains("#pragma code_page(65001)")); assert!(rc.contains("#include ")); assert!(rc.contains("#include ")); - assert!(rc.contains("#include \"common.ver\"")); + assert!(rc.contains("#include ")); assert!(rc.contains("VFT_DRV")); assert!(rc.contains("VFT2_DRV_SYSTEM")); assert!(rc.contains("\"test.sys\""));