commonlibsse_ng\rel\version/
win_api.rs

1// C++ Original code
2// - ref: https://github.com/SARDONYX-forks/CommonLibVR/blob/ng/src/REL/Version.cpp
3// SPDX-FileCopyrightText: (C) 2018 Ryan-rsm-McKenzie
4// SPDX-License-Identifier: MIT
5//
6// SPDX-FileCopyrightText: (C) 2025 SARDONYX
7// SPDX-License-Identifier: Apache-2.0 OR MIT
8
9use crate::rel::version::Version;
10
11/// Retrieves the file version of the specified executable or DLL.
12///
13/// # Errors
14/// - The [`lang-codepage`] part is **assumed to be an US English exe**, so if it is not an US English exe, the acquisition will fail.
15/// - It also fails if the version is not mixed in the exe.
16///
17/// # Example
18/// ```no_run
19/// use commonlibsse_ng::rel::version::{get_file_version, Version};
20/// use windows::core::h; // Windows UTF-16 string conversion macro
21///
22/// let target = h!(r"D:\STEAM\steamapps\common\Skyrim Special Edition\SkyrimSE.exe");
23/// let result = get_file_version(target);
24/// assert_eq!(result, Ok(Version::new(1, 6, 1170, 0)));
25/// ```
26///
27/// [`lang-codepage`]: https://learn.microsoft.com/windows/win32/api/winver/nf-winver-verqueryvaluew#stringfileinfolang-codepagestring-name
28pub fn get_file_version(filename: &windows::core::HSTRING) -> Result<Version, FileVersionError> {
29    // https://microsoft.github.io/windows-docs-rs/doc/windows/?search=GetFileVersionInfoSizeW
30    use core::ptr;
31    use windows::Win32::Storage::FileSystem::{
32        GetFileVersionInfoSizeW, GetFileVersionInfoW, VerQueryValueW,
33    };
34
35    let mut dummy = 0;
36    let size = unsafe { GetFileVersionInfoSizeW(filename, Some(&mut dummy)) };
37    if size == 0 {
38        return Err(FileVersionError::VersionInfoSize { filename: filename.to_string() });
39    }
40
41    let mut buf = vec![0_u8; size as usize];
42
43    if let Err(err) = unsafe { GetFileVersionInfoW(filename, None, size, buf.as_mut_ptr().cast()) }
44    {
45        return Err(FileVersionError::VersionInfoRetrieval { filename: filename.to_string(), err });
46    }
47
48    let ver_str = {
49        let buf_void_ptr = buf.as_mut_ptr().cast();
50        let query_path = windows::core::h!("\\StringFileInfo\\040904B0\\ProductVersion"); // NOTE: assumed 040904B0(US English, Unicode
51        let mut ver_buf = ptr::null_mut();
52        let mut ver_len: u32 = 0;
53        if !unsafe { VerQueryValueW(buf_void_ptr, query_path, &mut ver_buf, &mut ver_len) }
54            .as_bool()
55        {
56            return Err(FileVersionError::VersionQuery { filename: filename.to_string() });
57        }
58
59        let slice = unsafe { core::slice::from_raw_parts(ver_buf as *const u16, ver_len as usize) };
60        String::from_utf16_lossy(slice)
61    };
62
63    let mut version = Version::default_const();
64    for (i, token) in ver_str.split('.').take(4).enumerate() {
65        if let Ok(num) = token.parse::<u16>() {
66            version[i] = num;
67        }
68    }
69
70    Ok(version)
71}
72
73/// Error types for file version retrieval.
74#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, snafu::Snafu)]
75pub enum FileVersionError {
76    /// Failed to get file version info size for '{filename}'
77    VersionInfoSize { filename: String },
78
79    /// Failed to retrieve file version info for '{filename}', err: {err}
80    VersionInfoRetrieval { filename: String, err: windows::core::Error },
81
82    /// Failed to query product version for '{filename}'. (NOTE: If the target exe, dll exists, this error is probably due to the fact that it is not US English.)
83    VersionQuery { filename: String },
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use windows::core::h;
90
91    #[test]
92    fn test_valid_file_version() {
93        // Use a system file that is guaranteed to exist on Windows.
94        let target = h!("C:\\Windows\\splwow64.exe");
95        let version = get_file_version(target).unwrap_or_else(|err| panic!("{err}"));
96        dbg!(version);
97    }
98
99    #[test]
100    fn test_invalid_file_version() {
101        let target = h!("C:\\nonexistent_file.exe");
102        let result = get_file_version(target);
103
104        let expected_err = Err(FileVersionError::VersionInfoSize { filename: target.to_string() });
105
106        assert_eq!(result, expected_err);
107    }
108}