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}