Skip to main content

xll_utils/
pe.rs

1//! PE/COFF file parsing utilities.
2//!
3//! Provides functions for opening and parsing PE files, extracting
4//! architecture information, export directories, and individual exports.
5
6use std::path::Path;
7
8use object::pe::{ImageExportDirectory, IMAGE_FILE_DLL};
9use object::read::pe::{ExportTarget, ImageNtHeaders, ImageOptionalHeader};
10use object::LittleEndian as LE;
11
12use crate::error::{Error, Result};
13use crate::types::{Architecture, ExportDirectory, ExportInfo, PeFile};
14
15/// Open and parse a PE/COFF file from disk.
16///
17/// # Examples
18///
19/// ```no_run
20/// use xll_utils::pe::parse_pe_file;
21///
22/// let pe = parse_pe_file("my_xll.xll").unwrap();
23/// println!("{} exports", pe.exports().len());
24/// ```
25pub fn parse_pe_file(path: impl AsRef<Path>) -> Result<PeFile> {
26    let path = path.as_ref();
27    let data = std::fs::read(path).map_err(|e| Error::FileOpen {
28        path: path.to_path_buf(),
29        source: e,
30    })?;
31    let mut pe = parse_pe_bytes(&data)?;
32    pe.path = path.to_path_buf();
33    Ok(pe)
34}
35
36/// Parse a PE/COFF file from an in-memory byte slice.
37pub fn parse_pe_bytes(data: &[u8]) -> Result<PeFile> {
38    // Try PE32+ (64-bit) first since it's the most common for modern XLLs,
39    // then fall back to PE32 (32-bit).
40    if let Ok(pe) = object::read::pe::PeFile64::parse(data) {
41        return build_pe_file(&pe, data);
42    }
43    if let Ok(pe) = object::read::pe::PeFile32::parse(data) {
44        return build_pe_file(&pe, data);
45    }
46    Err(Error::InvalidFileFormat)
47}
48
49/// Extract all PE information from a parsed PE file (generic over 32/64-bit).
50fn build_pe_file<'data, Pe>(
51    pe: &object::read::pe::PeFile<'data, Pe, &'data [u8]>,
52    _data: &'data [u8],
53) -> Result<PeFile>
54where
55    Pe: ImageNtHeaders,
56{
57    let nt = pe.nt_headers();
58    let fh = nt.file_header();
59
60    let machine = fh.machine.get(LE);
61    let architecture = Architecture::from_machine_type(machine);
62
63    let characteristics = fh.characteristics.get(LE);
64    let is_dll = (characteristics & IMAGE_FILE_DLL) != 0;
65
66    let opt = nt.optional_header();
67    let image_base = opt.image_base();
68    let entry_point_rva = opt.address_of_entry_point();
69
70    // Parse exports.
71    let (export_directory, exports) = match pe.export_table() {
72        Ok(Some(et)) => parse_exports(&et)?,
73        Ok(None) => (None, Vec::new()),
74        Err(e) => return Err(Error::PeParse(format!("failed to read export table: {e}"))),
75    };
76
77    Ok(PeFile {
78        path: std::path::PathBuf::new(),
79        architecture,
80        is_dll,
81        image_base,
82        entry_point_rva,
83        export_directory,
84        exports,
85    })
86}
87
88/// Extract the export directory metadata and individual exports from an `ExportTable`.
89fn parse_exports(
90    et: &object::read::pe::ExportTable<'_>,
91) -> Result<(Option<ExportDirectory>, Vec<ExportInfo>)> {
92    let dir: &ImageExportDirectory = et.directory();
93
94    let export_directory = ExportDirectory {
95        characteristics: dir.characteristics.get(LE),
96        timestamp: dir.time_date_stamp.get(LE),
97        major_version: dir.major_version.get(LE),
98        minor_version: dir.minor_version.get(LE),
99        name_rva: dir.name.get(LE),
100        ordinal_base: dir.base.get(LE),
101        address_table_entries: dir.number_of_functions.get(LE),
102        number_of_name_pointers: dir.number_of_names.get(LE),
103        address_table_rva: dir.address_of_functions.get(LE),
104        name_pointer_rva: dir.address_of_names.get(LE),
105        ordinal_table_rva: dir.address_of_name_ordinals.get(LE),
106    };
107
108    // Use the high-level exports() iterator.
109    let raw_exports = et
110        .exports()
111        .map_err(|e| Error::PeParse(format!("failed to iterate exports: {e}")))?;
112
113    let mut exports = Vec::with_capacity(raw_exports.len());
114    for exp in &raw_exports {
115        let name = exp
116            .name
117            .map(|n| {
118                String::from_utf8(n.to_vec())
119                    .map_err(|e| Error::PeParse(format!("invalid UTF-8 in export name: {e}")))
120            })
121            .transpose()?;
122
123        let ordinal = exp.ordinal;
124
125        let (is_forwarded, forward_to, relative_address) = match exp.target {
126            ExportTarget::Address(rva) => (false, None, Some(rva)),
127            ExportTarget::ForwardByName(dll, fname) => {
128                let dll_s = String::from_utf8_lossy(dll);
129                let fname_s = String::from_utf8_lossy(fname);
130                (true, Some(format!("{dll_s}.{fname_s}")), None)
131            }
132            ExportTarget::ForwardByOrdinal(dll, ord) => {
133                let dll_s = String::from_utf8_lossy(dll);
134                (true, Some(format!("{dll_s}.#{ord}")), None)
135            }
136        };
137
138        exports.push(ExportInfo {
139            name,
140            ordinal,
141            is_forwarded,
142            forward_to,
143            relative_address,
144        });
145    }
146
147    Ok((Some(export_directory), exports))
148}
149
150impl PeFile {
151    /// Parse a PE file from the given path.
152    ///
153    /// ```no_run
154    /// use xll_utils::PeFile;
155    ///
156    /// let pe = PeFile::parse("my_xll.xll").unwrap();
157    /// assert!(pe.is_dll());
158    /// ```
159    pub fn parse(path: impl AsRef<Path>) -> Result<Self> {
160        parse_pe_file(path.as_ref())
161    }
162
163    /// Parse a PE file from an in-memory byte slice.
164    pub fn from_bytes(data: &[u8]) -> Result<Self> {
165        parse_pe_bytes(data)
166    }
167
168    /// Get the export directory, if present.
169    pub fn export_directory(&self) -> Option<&ExportDirectory> {
170        self.export_directory.as_ref()
171    }
172
173    /// Get all exports.
174    pub fn exports(&self) -> &[ExportInfo] {
175        &self.exports
176    }
177
178    /// Get the CPU architecture.
179    pub fn architecture(&self) -> Architecture {
180        self.architecture
181    }
182
183    /// Check if this is a DLL file.
184    pub fn is_dll(&self) -> bool {
185        self.is_dll
186    }
187
188    /// Find an export by name.
189    pub fn find_export(&self, name: &str) -> Option<&ExportInfo> {
190        self.exports.iter().find(|e| e.name.as_deref() == Some(name))
191    }
192
193    /// Find an export by ordinal.
194    pub fn find_export_by_ordinal(&self, ordinal: u32) -> Option<&ExportInfo> {
195        self.exports.iter().find(|e| e.ordinal == ordinal)
196    }
197
198    /// Get export names as a sorted vector.
199    pub fn export_names(&self) -> Vec<String> {
200        let mut names: Vec<_> = self
201            .exports
202            .iter()
203            .filter_map(|e| e.name.clone())
204            .collect();
205        names.sort();
206        names
207    }
208}