Skip to main content

everything_ipc/
pe.rs

1/*!
2PE file utilities.
3
4APIs:
5- [`Version::from_exe()`]
6  - [`Version::from_current_exe()`]
7*/
8
9use std::{io, path::Path};
10
11use pelite::{PeFile, pe32::image::VS_FIXEDFILEINFO};
12
13use crate::Version;
14
15/// Error type for PE version extraction.
16#[derive(Debug, thiserror::Error)]
17pub enum PeVersionError {
18    #[error("failed to read: {0}")]
19    Io(#[from] io::Error),
20
21    #[error("failed to parse PE: {0}")]
22    Parse(#[from] pelite::Error),
23
24    #[error("failed to find version: {0}")]
25    Resources(#[from] pelite::resources::FindError),
26
27    #[error("version not found")]
28    NotFound,
29}
30
31impl Version {
32    /// Get the version information from the current executable.
33    ///
34    /// Mainly for plugin usage. Can be used before plugin init.
35    ///
36    /// # Returns
37    ///
38    /// `Ok(version)` if version info is found, or `Err(e)` if no version info or reading/parsing fails.
39    ///
40    /// # Example
41    ///
42    /// ```no_run
43    /// let version = everything_ipc::Version::from_current_exe()?;
44    /// println!("Version: {:?}", version);
45    /// # Result::<_, Box<dyn std::error::Error>>::Ok(())
46    /// ```
47    pub fn from_current_exe() -> Result<Self, PeVersionError> {
48        let exe = std::env::current_exe()?;
49        Self::from_exe(&exe)
50    }
51
52    /// Get the version information from a PE executable.
53    ///
54    /// # Arguments
55    ///
56    /// - `exe_path` - Path to the PE executable file.
57    ///
58    /// # Returns
59    ///
60    /// `Ok(version)` if version info is found, or `Err(e)` if no version info or reading/parsing fails.
61    ///
62    /// # Example
63    ///
64    /// ```no_run
65    /// let version = everything_ipc::Version::from_exe(std::path::Path::new(r"C:\Path\To\Everything.exe"))?;
66    /// println!("Version: {:?}", version);
67    /// # Result::<_, Box<dyn std::error::Error>>::Ok(())
68    /// ```
69    pub fn from_exe(exe_path: &Path) -> Result<Self, PeVersionError> {
70        let image = std::fs::read(exe_path)?;
71        let pe = PeFile::from_bytes(&image)?;
72        let resources = pe.resources()?;
73        let version_info = resources.version_info()?;
74
75        let fixed = version_info.fixed();
76        let (major, minor, patch, build) = decode_version(fixed);
77
78        // Return error if all version components are zero
79        if major == 0 && minor == 0 && patch == 0 && build == 0 {
80            Err(PeVersionError::NotFound)
81        } else {
82            Ok(Self {
83                major,
84                minor,
85                revision: patch,
86                build,
87            })
88        }
89    }
90}
91
92/// Decode version from VS_FIXEDFILEINFO.
93fn decode_version(fixed: Option<&VS_FIXEDFILEINFO>) -> (u32, u32, u32, u32) {
94    match fixed {
95        Some(info) => {
96            // Prefer product version, fall back to file version if product is all zeros
97            let version = if info.dwProductVersion.Major == 0
98                && info.dwProductVersion.Minor == 0
99                && info.dwProductVersion.Patch == 0
100                && info.dwProductVersion.Build == 0
101            {
102                info.dwFileVersion
103            } else {
104                info.dwProductVersion
105            };
106
107            // The order is different from its physical layout
108            (
109                version.Major as u32,
110                version.Minor as u32,
111                version.Patch as u32,
112                version.Build as u32,
113            )
114        }
115        None => (0, 0, 0, 0),
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[ignore]
124    #[test]
125    fn get_exe_version() {
126        let version = Version::from_exe(Path::new(
127            r"../tests/everything/v1.5.0.1403_x64/Everything.exe",
128        ))
129        .expect("read exe");
130        assert_eq!(version.tuple(), (1, 5, 0, 1403));
131    }
132}