use std::collections::HashMap;
use std::fs;
use std::path::Path;
use std::path::Path as StdPath;
use object::endian::LittleEndian as LE;
use object::pe;
use object::read::FileKind;
use packageurl::PackageUrl;
use crate::models::{DatasourceId, PackageData, PackageType, Party};
use crate::parser_warn as warn;
use crate::register_parser;
use super::ParsePackagesResult;
use super::license_normalization::{
detect_declared_license_from_text, normalize_spdx_declared_license,
};
use super::utils::{MAX_ITERATION_COUNT, truncate_field};
register_parser!(
"Windows PE executable with VERSIONINFO package metadata",
&["<windows executable and DLL files with VERSIONINFO resources>"],
"winexe",
"",
Some("https://learn.microsoft.com/en-us/windows/win32/menurc/versioninfo-resource"),
);
const VS_FIXEDFILEINFO_SIGNATURE: u32 = 0xFEEF04BD;
const MAX_SIBLING_LICENSE_BYTES: u64 = 256 * 1024;
const WINDOWS_VERSION_FALLBACK_KEYS: &[&str] = &[
"ProductName",
"FileDescription",
"CompanyName",
"LegalCopyright",
"ProductVersion",
"FileVersion",
"OriginalFilename",
"InternalName",
"URL",
"WWW",
"License",
];
#[derive(Debug, Clone)]
struct FixedVersionInfo {
product_version: Option<String>,
}
#[derive(Debug, Clone, Default)]
struct ParsedVersionInfo {
string_tables: Vec<HashMap<String, String>>,
fixed: Option<FixedVersionInfo>,
}
type VersionStrings<'a> = &'a HashMap<String, String>;
type PreferredVersionStringSources<'a> = (Option<VersionStrings<'a>>, Option<VersionStrings<'a>>);
#[derive(Debug, Clone)]
struct VersionBlock<'a> {
key: String,
value_type: u16,
value: &'a [u8],
children: &'a [u8],
}
pub(crate) fn try_parse_windows_executable_bytes(
path: &Path,
bytes: &[u8],
) -> Option<ParsePackagesResult> {
let packages = parse_windows_executable_bytes(path, bytes);
(!packages.is_empty()).then_some(ParsePackagesResult {
packages,
scan_diagnostics: Vec::new(),
scan_errors: Vec::new(),
})
}
pub(crate) fn extract_windows_executable_metadata_text(bytes: &[u8]) -> Option<String> {
let parsed = match FileKind::parse(bytes) {
Ok(FileKind::Pe32) => parse_pe_version_info::<pe::ImageNtHeaders32>(bytes),
Ok(FileKind::Pe64) => parse_pe_version_info::<pe::ImageNtHeaders64>(bytes),
_ => return None,
}?;
let fallback_strings = extract_utf16_version_string_fallback(bytes);
let mut lines = Vec::new();
for string_table in &parsed.string_tables {
for key in [
"ProductName",
"FileDescription",
"CompanyName",
"LegalCopyright",
"License",
"LegalTrademarks",
"LegalTrademarks1",
"LegalTrademarks2",
"LegalTrademarks3",
"Comments",
"URL",
"WWW",
] {
if let Some(value) = string_table.get(key).map(|value| value.trim())
&& !value.is_empty()
{
let line = format!("{key}: {value}");
if !lines.contains(&line) {
lines.push(line);
}
}
}
}
if !fallback_strings.is_empty() {
for key in [
"ProductName",
"FileDescription",
"CompanyName",
"LegalCopyright",
"License",
"LegalTrademarks",
"LegalTrademarks1",
"LegalTrademarks2",
"LegalTrademarks3",
"Comments",
"URL",
"WWW",
] {
if let Some(value) = fallback_strings.get(key).map(|value| value.trim())
&& !value.is_empty()
{
let line = format!("{key}: {value}");
if !lines.contains(&line) {
lines.push(line);
}
}
}
}
if let Some(version) = parsed.fixed.and_then(|fixed| fixed.product_version) {
let line = format!("ProductVersion: {version}");
if !lines.contains(&line) {
lines.push(line);
}
}
(!lines.is_empty()).then(|| lines.join("\n"))
}
fn parse_windows_executable_bytes(path: &Path, bytes: &[u8]) -> Vec<PackageData> {
let fallback_strings = extract_utf16_version_string_fallback(bytes);
let parsed = match FileKind::parse(bytes) {
Ok(FileKind::Pe32) => parse_pe_version_info::<pe::ImageNtHeaders32>(bytes),
Ok(FileKind::Pe64) => parse_pe_version_info::<pe::ImageNtHeaders64>(bytes),
_ => return Vec::new(),
};
match parsed {
Some(version_info) => build_windows_executable_package(
path,
version_info,
(!fallback_strings.is_empty()).then_some(&fallback_strings),
)
.into_iter()
.collect(),
None if !fallback_strings.is_empty() => build_windows_executable_package(
path,
ParsedVersionInfo::default(),
Some(&fallback_strings),
)
.into_iter()
.collect(),
None => build_windows_executable_fallback(path)
.into_iter()
.collect(),
}
}
fn parse_pe_version_info<Pe: object::read::pe::ImageNtHeaders>(
bytes: &[u8],
) -> Option<ParsedVersionInfo> {
let pe = object::read::pe::PeFile::<Pe>::parse(bytes).ok()?;
let resource_directory = pe
.data_directories()
.resource_directory(bytes, &pe.section_table())
.ok()??;
let root = resource_directory.root().ok()?;
let version_entry = root.entries.iter().find(|entry| {
matches!(entry.name_or_id(), object::read::pe::ResourceNameOrId::Id(id) if id == pe::RT_VERSION)
})?;
let name_table = version_entry.data(resource_directory).ok()?.table()?;
let parsed_infos = name_table
.entries
.iter()
.filter_map(|name_entry| name_entry.data(resource_directory).ok()?.table())
.flat_map(|language_table| {
language_table.entries.iter().filter_map(|language_entry| {
let data_entry = language_entry.data(resource_directory).ok()?.data()?;
let version_bytes = resource_data_bytes(&pe, bytes, data_entry)?;
parse_version_info_bytes(version_bytes)
})
});
merge_parsed_version_infos(parsed_infos)
}
fn resource_data_bytes<'a, Pe: object::read::pe::ImageNtHeaders>(
pe_file: &object::read::pe::PeFile<'a, Pe>,
bytes: &'a [u8],
data_entry: &pe::ImageResourceDataEntry,
) -> Option<&'a [u8]> {
let data_rva = data_entry.offset_to_data.get(LE);
let size = data_entry.size.get(LE) as usize;
pe_file
.section_table()
.pe_data_at(bytes, data_rva)
.and_then(|data| data.get(..size))
}
fn parse_version_info_bytes(bytes: &[u8]) -> Option<ParsedVersionInfo> {
let root = parse_version_block(bytes)?;
if root.key != "VS_VERSION_INFO" {
return None;
}
let mut parsed = ParsedVersionInfo {
fixed: parse_fixed_version_info(root.value),
..ParsedVersionInfo::default()
};
for child in iter_version_blocks(root.children) {
let Some(child) = child else {
continue;
};
if child.key != "StringFileInfo" {
continue;
}
for string_table in iter_version_blocks(child.children) {
let Some(string_table) = string_table else {
continue;
};
let mut strings = HashMap::new();
for string_entry in iter_version_blocks(string_table.children) {
let Some(string_entry) = string_entry else {
continue;
};
let Some(value) = decode_version_value(&string_entry) else {
continue;
};
if !string_entry.key.is_empty() && !value.is_empty() {
strings.insert(string_entry.key.clone(), value);
}
}
if !strings.is_empty() {
parsed.string_tables.push(strings);
}
}
}
Some(parsed)
}
fn parse_version_block(bytes: &[u8]) -> Option<VersionBlock<'_>> {
if bytes.len() < 6 {
return None;
}
let total_len = read_u16_le(bytes, 0)? as usize;
let value_len = read_u16_le(bytes, 2)? as usize;
let value_type = read_u16_le(bytes, 4)?;
if total_len == 0 || total_len > bytes.len() {
return None;
}
let block_bytes = &bytes[..total_len];
let mut cursor = 6;
let key_end = find_utf16_nul(block_bytes.get(cursor..)?)?;
let key_bytes = &block_bytes[cursor..cursor + key_end];
let key = decode_utf16_bytes(key_bytes)?;
cursor += key_end + 2;
cursor = align_to_4(cursor);
if cursor > block_bytes.len() {
return None;
}
let value_byte_len = if value_type == 1 {
value_len.checked_mul(2)?
} else {
value_len
};
let value_end = cursor.checked_add(value_byte_len)?;
if value_end > block_bytes.len() {
return None;
}
let value = &block_bytes[cursor..value_end];
let children_start = align_to_4(value_end);
let children = block_bytes.get(children_start..).unwrap_or(&[]);
Some(VersionBlock {
key,
value_type,
value,
children,
})
}
fn merge_parsed_version_infos(
parsed_infos: impl IntoIterator<Item = ParsedVersionInfo>,
) -> Option<ParsedVersionInfo> {
let mut merged = ParsedVersionInfo::default();
let mut saw_any = false;
for parsed in parsed_infos {
saw_any = true;
if merged.fixed.is_none() {
merged.fixed = parsed.fixed;
}
merged.string_tables.extend(parsed.string_tables);
}
saw_any.then_some(merged)
}
fn extract_utf16_version_string_fallback(bytes: &[u8]) -> HashMap<String, String> {
let units = bytes
.chunks_exact(2)
.map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
.collect::<Vec<_>>();
let text = String::from_utf16_lossy(&units);
WINDOWS_VERSION_FALLBACK_KEYS
.iter()
.filter_map(|key| {
find_utf16_version_value(&text, key).map(|value| ((*key).to_string(), value))
})
.collect()
}
fn find_utf16_version_value(text: &str, key: &str) -> Option<String> {
let needle = format!("{key}\0");
let start = text.find(&needle)? + needle.len();
let rest = text.get(start..)?.trim_start_matches('\0');
let value_end = rest.find('\0')?;
let value = rest[..value_end].trim();
(!value.is_empty()).then(|| value.to_string())
}
fn iter_version_blocks(mut bytes: &[u8]) -> impl Iterator<Item = Option<VersionBlock<'_>>> + '_ {
let mut count = 0usize;
std::iter::from_fn(move || {
if bytes.is_empty() {
return None;
}
count += 1;
if count > MAX_ITERATION_COUNT {
warn!(
"iter_version_blocks exceeded MAX_ITERATION_COUNT ({MAX_ITERATION_COUNT}), stopping iteration"
);
return None;
}
let block_len = read_u16_le(bytes, 0)? as usize;
if block_len == 0 {
return None;
}
let current = bytes.get(..block_len)?;
let next_offset = align_to_4(block_len);
bytes = bytes.get(next_offset..).unwrap_or(&[]);
Some(parse_version_block(current))
})
}
fn parse_fixed_version_info(value: &[u8]) -> Option<FixedVersionInfo> {
if value.len() < 13 * 4 {
return None;
}
let signature = read_u32_le(value, 0)?;
if signature != VS_FIXEDFILEINFO_SIGNATURE {
return None;
}
let product_version_ms = read_u32_le(value, 16)?;
let product_version_ls = read_u32_le(value, 20)?;
let product_version = version_components_to_string(product_version_ms, product_version_ls);
Some(FixedVersionInfo { product_version })
}
fn version_components_to_string(ms: u32, ls: u32) -> Option<String> {
let major = (ms >> 16) & 0xFFFF;
let minor = ms & 0xFFFF;
let patch = (ls >> 16) & 0xFFFF;
let build = ls & 0xFFFF;
let version = format!("{major}.{minor}.{patch}.{build}");
(version != "0.0.0.0").then_some(version)
}
fn decode_version_value(block: &VersionBlock<'_>) -> Option<String> {
if block.value_type != 1 {
return None;
}
let mut value = decode_utf16_bytes(block.value)?;
while value.ends_with('