use std::borrow::Cow;
use std::collections::HashMap;
use std::ffi::c_void;
use std::io;
use std::mem;
use std::path::Path;
use std::ptr;
use wincorda::prelude::*;
use windows_sys::Win32::Globalization::GetUserDefaultUILanguage;
use windows_sys::Win32::Storage::FileSystem::{
GetFileVersionInfoSizeW, GetFileVersionInfoW, VS_FF_DEBUG, VS_FF_INFOINFERRED, VS_FF_PATCHED,
VS_FF_PRERELEASE, VS_FF_PRIVATEBUILD, VS_FF_SPECIALBUILD, VS_FIXEDFILEINFO, VerQueryValueW,
};
use windows_sys::Win32::System::SystemServices::{LANG_ENGLISH, SUBLANG_ENGLISH_US};
const fn make_langid(primary: u32, sub: u32) -> u16 {
((sub << 10) | primary) as u16
}
const CP_UNICODE: u16 = 1200;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(C, align(1))]
pub struct Translation {
pub language: u16,
pub code_page: u16,
}
const _: () = {
assert!(mem::size_of::<Translation>() == 4);
assert!(mem::offset_of!(Translation, language) == 0);
assert!(mem::offset_of!(Translation, code_page) == 2);
};
impl Default for Translation {
fn default() -> Self {
Self {
language: make_langid(LANG_ENGLISH, SUBLANG_ENGLISH_US),
code_page: CP_UNICODE,
}
}
}
impl Translation {
#[must_use]
pub fn from_system() -> Self {
Self {
language: unsafe { GetUserDefaultUILanguage() },
code_page: CP_UNICODE,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct FileInformation {
pub comments: Option<String>,
pub company_name: Option<String>,
pub file_description: Option<String>,
pub file_version: Option<String>,
pub internal_name: Option<String>,
pub legal_copyright: Option<String>,
pub legal_trademarks: Option<String>,
pub original_filename: Option<String>,
pub private_build: Option<String>,
pub product_name: Option<String>,
pub product_version: Option<String>,
pub special_build: Option<String>,
}
macro_rules! string_getters {
($($field:ident),* $(,)?) => {
$(
pub fn $field(&self) -> Option<&str> {
self.$field.as_deref()
}
)*
};
}
impl FileInformation {
string_getters!(
comments,
company_name,
file_description,
file_version,
internal_name,
legal_copyright,
legal_trademarks,
original_filename,
private_build,
product_name,
product_version,
special_build,
);
#[must_use]
pub fn meaningful_product_name(&self) -> Option<&str> {
let raw = self.product_name()?;
if raw == "Microsoft\u{ae} Windows\u{ae} Operating System" {
return None;
}
Some(raw)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct FixedFileInfo {
pub struct_version: u32,
pub file_major_part: u16,
pub file_minor_part: u16,
pub file_build_part: u16,
pub file_private_part: u16,
pub product_major_part: u16,
pub product_minor_part: u16,
pub product_build_part: u16,
pub product_private_part: u16,
pub file_flags_mask: u32,
pub is_debug: bool,
pub is_patched: bool,
pub is_pre_release: bool,
pub is_private_build: bool,
pub is_special_build: bool,
pub is_info_inferred: bool,
pub file_os: u32,
pub file_type: u32,
pub file_subtype: i32,
pub file_date: u64,
}
pub struct FileVersionInfo {
by_translation: HashMap<Translation, FileInformation>,
fixed: Option<FixedFileInfo>,
}
impl FileVersionInfo {
pub fn load(path: &Path) -> io::Result<Self> {
let path_w = wide(path.to_string_lossy());
let buffer = read_block(&path_w)?;
let by_translation = read_translations(&buffer)
.into_iter()
.map(|t| (t, read_strings(&buffer, t)))
.collect();
let fixed = read_fixed(&buffer);
Ok(Self {
by_translation,
fixed,
})
}
#[must_use]
pub fn for_translation(&self, translation: Translation) -> Option<&FileInformation> {
self.by_translation
.get(&translation)
.or_else(|| self.by_translation.get(&Translation::from_system())) .or_else(|| self.by_translation.get(&Translation::default())) .or_else(|| self.by_translation.values().next()) }
#[must_use]
pub fn english(&self) -> Option<&FileInformation> {
self.for_translation(Translation::default())
}
#[must_use]
pub fn system(&self) -> Option<&FileInformation> {
self.for_translation(Translation::from_system())
}
#[must_use]
pub const fn all(&self) -> &HashMap<Translation, FileInformation> {
&self.by_translation
}
#[must_use]
pub const fn fixed(&self) -> Option<&FixedFileInfo> {
self.fixed.as_ref()
}
}
fn wide<'a>(s: impl Into<Cow<'a, str>>) -> NullTerminated<'static, WCHAR> {
NullTerminated::from(s.into())
}
fn read_block(path_w: &NullTerminated<'static, WCHAR>) -> io::Result<Vec<u8>> {
let size = unsafe { GetFileVersionInfoSizeW(path_w.as_ptr(), std::ptr::null_mut()) };
if size == 0 {
return Err(io::Error::last_os_error());
}
let mut buffer = vec![0u8; size as usize];
let ok = unsafe { GetFileVersionInfoW(path_w.as_ptr(), 0, size, buffer.as_mut_ptr().cast()) };
if ok == 0 {
return Err(io::Error::last_os_error());
}
Ok(buffer)
}
fn read_translations(buffer: &[u8]) -> Vec<Translation> {
let sub_block = wide("\\VarFileInfo\\Translation");
let mut data: *mut c_void = ptr::null_mut();
let mut len: u32 = 0;
let ok = unsafe {
VerQueryValueW(
buffer.as_ptr().cast::<c_void>(),
sub_block.as_ptr(),
&raw mut data,
&raw mut len,
)
};
if ok == 0 || data.is_null() || len == 0 {
return Vec::new();
}
let count = (len as usize) / mem::size_of::<Translation>();
let pairs = unsafe { std::slice::from_raw_parts(data as *const Translation, count) };
pairs.to_vec()
}
fn read_strings(buffer: &[u8], translation: Translation) -> FileInformation {
let prefix = format!(
"\\StringFileInfo\\{:04x}{:04x}\\",
translation.language, translation.code_page,
);
let q = |name: &str| read_string(buffer, &prefix, name);
FileInformation {
comments: q("Comments"),
company_name: q("CompanyName"),
file_description: q("FileDescription"),
file_version: q("FileVersion"),
internal_name: q("InternalName"),
legal_copyright: q("LegalCopyright"),
legal_trademarks: q("LegalTrademarks"),
original_filename: q("OriginalFilename"),
private_build: q("PrivateBuild"),
product_name: q("ProductName"),
product_version: q("ProductVersion"),
special_build: q("SpecialBuild"),
}
}
fn read_string(buffer: &[u8], prefix: &str, name: &str) -> Option<String> {
let path = wide(format!("{prefix}{name}"));
let mut data: *mut c_void = ptr::null_mut();
let mut len: u32 = 0;
let ok = unsafe {
VerQueryValueW(
buffer.as_ptr().cast::<c_void>(),
path.as_ptr(),
&raw mut data,
&raw mut len,
)
};
if ok == 0 || data.is_null() || len == 0 {
return None;
}
let nt = NullTerminated::<WCHAR>::try_from(data as *const WCHAR).ok()?;
Some(String::from(nt))
}
fn read_fixed(buffer: &[u8]) -> Option<FixedFileInfo> {
let sub_block = wide("\\");
let mut data: *mut c_void = ptr::null_mut();
let mut len: u32 = 0;
let ok = unsafe {
VerQueryValueW(
buffer.as_ptr().cast::<c_void>(),
sub_block.as_ptr(),
&raw mut data,
&raw mut len,
)
};
if ok == 0 || data.is_null() || (len as usize) < mem::size_of::<VS_FIXEDFILEINFO>() {
return None;
}
let v: &VS_FIXEDFILEINFO = unsafe { &*(data as *const VS_FIXEDFILEINFO) };
let masked = v.dwFileFlags & v.dwFileFlagsMask;
Some(FixedFileInfo {
struct_version: v.dwStrucVersion,
file_major_part: hi_word(v.dwFileVersionMS),
file_minor_part: lo_word(v.dwFileVersionMS),
file_build_part: hi_word(v.dwFileVersionLS),
file_private_part: lo_word(v.dwFileVersionLS),
product_major_part: hi_word(v.dwProductVersionMS),
product_minor_part: lo_word(v.dwProductVersionMS),
product_build_part: hi_word(v.dwProductVersionLS),
product_private_part: lo_word(v.dwProductVersionLS),
file_flags_mask: v.dwFileFlagsMask,
is_debug: masked & VS_FF_DEBUG != 0,
is_patched: masked & VS_FF_PATCHED != 0,
is_pre_release: masked & VS_FF_PRERELEASE != 0,
is_private_build: masked & VS_FF_PRIVATEBUILD != 0,
is_special_build: masked & VS_FF_SPECIALBUILD != 0,
is_info_inferred: masked & VS_FF_INFOINFERRED != 0,
file_os: v.dwFileOS,
file_type: v.dwFileType,
file_subtype: v.dwFileSubtype as i32,
file_date: (u64::from(v.dwFileDateMS) << 32) | u64::from(v.dwFileDateLS),
})
}
const fn hi_word(d: u32) -> u16 {
(d >> 16) as u16
}
const fn lo_word(d: u32) -> u16 {
(d & 0xFFFF) as u16
}