• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

loot / loot-condition-interpreter / 19803524316

30 Nov 2025 07:07PM UTC coverage: 89.921%. Remained the same
19803524316

push

github

Ortham
Remove pelite dependency

The advantages of using object instead of pelite are:

- It's 1 dependency instead of 8
- It's 57474 unaudited lines of code instead of 240715 (though if I swap out winapi for windows-sys that drops to 59392, since windows-sys is also used by other dependencies)
- object seems to be better maintained - it's had a few releases this year, while pelite hasn't had any in 3 years, and my PR for replacing winapi hasn't had any response in the month and a half it's been open
- object is more popular, with 238 dependents vs 33, and though it's about half a year newer it's got about 600x more downloads

I'm not sure how much heavy lifting object is doing for me - it might not be too much effort to do everything I need without it.

6 of 6 new or added lines in 1 file covered. (100.0%)

13 existing lines in 2 files now uncovered.

4345 of 4832 relevant lines covered (89.92%)

29.7 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.93
/src/function/version/pe.rs
1
use std::path::Path;
2

3
use object::{
4
    pe::{ImageDataDirectory, ImageResourceDataEntry, IMAGE_DIRECTORY_ENTRY_RESOURCE},
5
    read::pe::{ImageNtHeaders, PeFile32, PeFile64, ResourceDirectoryEntryData},
6
    LittleEndian,
7
};
8

9
use crate::Error;
10

11
use super::{ReleaseId, Version};
12

13
const KEY_OFFSET: usize = 6;
14

15
struct StructHeaders {
16
    length: usize,
17
    value_length: usize,
18
}
19

20
enum ReadResult {
21
    Version(String),
22
    NewOffset(usize),
23
}
24

25
pub(super) fn read_version_info_data(file_path: &Path) -> Result<Option<Vec<u8>>, Error> {
40✔
26
    let file =
31✔
27
        std::fs::File::open(file_path).map_err(|e| Error::IoError(file_path.to_path_buf(), e))?;
40✔
28
    let reader = object::ReadCache::new(std::io::BufReader::new(file));
31✔
29

30
    let file_kind = object::FileKind::parse(&reader)
31✔
31
        .map_err(|e| Error::PeParsingError(file_path.to_path_buf(), Box::new(e)))?;
31✔
32

33
    match file_kind {
22✔
34
        object::FileKind::Pe32 => {
35
            let file = PeFile32::parse(&reader)
18✔
36
                .map_err(|e| Error::PeParsingError(file_path.to_path_buf(), Box::new(e)))?;
18✔
37

38
            read_version_info_data_from_file(&file)
18✔
39
                .map_err(|e| Error::PeParsingError(file_path.to_path_buf(), e))
18✔
40
        }
41
        object::FileKind::Pe64 => {
42
            let file = PeFile64::parse(&reader)
4✔
43
                .map_err(|e| Error::PeParsingError(file_path.to_path_buf(), Box::new(e)))?;
4✔
44

45
            read_version_info_data_from_file(&file)
4✔
46
                .map_err(|e| Error::PeParsingError(file_path.to_path_buf(), e))
4✔
47
        }
48
        _ => Err(Error::PeParsingError(
×
49
            file_path.to_path_buf(),
×
50
            "Invalid PE optional header magic".into(),
×
51
        )),
×
52
    }
53
}
40✔
54

55
fn read_version_info_data_from_file<'a, T: ImageNtHeaders, R: object::ReadRef<'a>>(
22✔
56
    file: &object::read::pe::PeFile<'a, T, R>,
22✔
57
) -> Result<Option<Vec<u8>>, Box<dyn std::error::Error + Send + Sync + 'static>> {
22✔
58
    const RT_VERSION: u16 = 16;
59

60
    let Some(resources) = file
22✔
61
        .data_directories()
22✔
62
        .resource_directory(file.data(), &file.section_table())?
22✔
63
    else {
64
        return Ok(None);
×
65
    };
66

67
    let resources_root = resources.root()?;
22✔
68

69
    let Some(resource_directory_entry) =
22✔
70
        file.data_directories().get(IMAGE_DIRECTORY_ENTRY_RESOURCE)
22✔
71
    else {
72
        return Ok(None);
×
73
    };
74

75
    // The entries in the root level determine the resource type ID.
76
    for type_table_entry in resources_root.entries {
26✔
77
        if type_table_entry.name_or_id().id() == Some(RT_VERSION) {
22✔
78
            let data = type_table_entry.data(resources)?;
18✔
79

80
            if let ResourceDirectoryEntryData::Table(name_table) = data {
18✔
81
                // The entries in the second level determine the resource name.
82
                for name_table_entry in name_table.entries {
18✔
83
                    let data = name_table_entry.data(resources)?;
18✔
84

85
                    if let ResourceDirectoryEntryData::Table(language_table) = data {
18✔
86
                        // The entries in the second level determine the resource language.
87
                        for language_table_entry in language_table.entries {
18✔
88
                            let data = language_table_entry.data(resources)?;
18✔
89

90
                            if let ResourceDirectoryEntryData::Data(data) = data {
18✔
91
                                return read_version_table_data(
18✔
92
                                    file,
18✔
93
                                    *resource_directory_entry,
18✔
94
                                    data,
18✔
95
                                );
96
                            }
×
97
                        }
98
                    }
×
99
                }
100
            }
×
101
        }
4✔
102
    }
103

104
    Ok(None)
4✔
105
}
22✔
106

107
fn read_version_table_data<'a, T: ImageNtHeaders, R: object::ReadRef<'a>>(
18✔
108
    file: &object::read::pe::PeFile<'a, T, R>,
18✔
109
    resource_directory_entry: ImageDataDirectory,
18✔
110
    data: &ImageResourceDataEntry,
18✔
111
) -> Result<Option<Vec<u8>>, Box<dyn std::error::Error + Send + Sync + 'static>> {
18✔
112
    let bytes = resource_directory_entry.data(file.data(), &file.section_table())?;
18✔
113

114
    let offset_start = to_usize(
18✔
115
        data.offset_to_data.get(LittleEndian) - resource_directory_entry.address_range().0,
18✔
116
    );
117
    let bytes_subslice = subslice(bytes, offset_start, to_usize(data.size.get(LittleEndian)))?;
18✔
118

119
    Ok(Some(bytes_subslice.to_vec()))
18✔
120
}
18✔
121

122
#[expect(
123
    clippy::as_conversions,
124
    reason = "A compile-time assertion ensures that this conversion will be lossless on all relevant target platforms"
125
)]
126
const fn to_usize(value: u32) -> usize {
36✔
127
    // Error at compile time if this conversion isn't lossless.
128
    const _: () = assert!(u32::BITS <= usize::BITS, "cannot fit a u32 into a usize!");
129
    value as usize
36✔
130
}
36✔
131

132
fn subslice(bytes: &[u8], offset: usize, size: usize) -> Result<&[u8], String> {
64✔
133
    bytes.get(offset..offset + size).ok_or_else(|| {
64✔
134
        format!("Invalid subslice of size {size} at offset {offset} of bytes {bytes:X?}")
×
135
    })
×
136
}
64✔
137

138
// <https://learn.microsoft.com/en-us/windows/win32/menurc/vs-versioninfo>
139
pub(super) fn read_file_version(data: &[u8]) -> Result<Option<Version>, String> {
6✔
140
    let StructHeaders { value_length, .. } = read_vs_version_info_headers(data)?;
6✔
141

142
    if value_length == 0 {
6✔
143
        Ok(None)
×
144
    } else {
145
        let fixed_file_info = subslice(data, 40, value_length)?;
6✔
146
        Some(read_vs_fixed_file_info(fixed_file_info)).transpose()
6✔
147
    }
148
}
6✔
149

150
fn read_vs_version_info_headers(data: &[u8]) -> Result<StructHeaders, String> {
16✔
151
    let headers = read_struct_headers(data)?;
16✔
152

153
    if headers.length != data.len() {
16✔
154
        return Err(format!(
×
155
            "Unexpected length of VS_VERSIONINFO struct, got {} but buffer length is {}",
×
156
            headers.length,
×
157
            data.len()
×
158
        ));
×
159
    }
16✔
160

161
    if !has_subslice_at(
16✔
162
        data,
16✔
163
        KEY_OFFSET,
16✔
164
        b"V\0S\0_\0V\0E\0R\0S\0I\0O\0N\0_\0I\0N\0F\0O\0\0\0",
16✔
165
    ) {
16✔
166
        return Err(format!(
×
167
            "The szKey field's value is not valid for a VS_VERSIONINFO struct: {data:X?}"
×
168
        ));
×
169
    }
16✔
170

171
    Ok(headers)
16✔
172
}
16✔
173

174
fn read_struct_headers(data: &[u8]) -> Result<StructHeaders, String> {
46✔
175
    let [l0, l1, vl0, vl1, ..] = data else {
46✔
176
        return Err(format!(
×
177
            "The buffer was too small to hold a struct: {data:X?}"
×
178
        ));
×
179
    };
180

181
    let length = usize::from(u16::from_le_bytes([*l0, *l1]));
46✔
182

183
    let value_length = usize::from(u16::from_le_bytes([*vl0, *vl1]));
46✔
184

185
    Ok(StructHeaders {
46✔
186
        length,
46✔
187
        value_length,
46✔
188
    })
46✔
189
}
46✔
190

191
fn has_subslice_at(haystack: &[u8], offset: usize, needle: &[u8]) -> bool {
62✔
192
    haystack
62✔
193
        .get(offset..offset + needle.len())
62✔
194
        .is_some_and(|s| s == needle)
62✔
195
}
62✔
196

197
// <learn.microsoft.com/en-us/windows/win32/api/verrsrc/ns-verrsrc-vs_fixedfileinfo>
198
fn read_vs_fixed_file_info(data: &[u8]) -> Result<Version, String> {
6✔
199
    const VS_FIXEDFILEINFO_SIZE: usize = 0x34;
200
    const VS_FIXEDFILEINFO_SIGNATURE: [u8; 4] = [0xBD, 0x04, 0xEF, 0xFE];
201
    const FILE_VERSION_OFFSET: usize = 8;
202
    const FILE_VERSION_LENGTH: usize = 8;
203

204
    if data.len() != VS_FIXEDFILEINFO_SIZE {
6✔
205
        return Err(format!(
×
206
            "Unexpected length of VS_VERSIONINFO value, got {} but expected {}",
×
207
            data.len(),
×
208
            VS_FIXEDFILEINFO_SIZE
×
209
        ));
×
210
    }
6✔
211

212
    if !has_subslice_at(data, 0, &VS_FIXEDFILEINFO_SIGNATURE) {
6✔
213
        return Err(format!(
×
214
            "Unexpected first four bytes of VS_VERSIONINFO struct {data:X?}"
×
215
        ));
×
216
    }
6✔
217

218
    let Some(
219
        [file_minor_0, file_minor_1, file_major_0, file_major_1, file_build_0, file_build_1, file_patch_0, file_patch_1],
6✔
220
    ) = data.get(FILE_VERSION_OFFSET..FILE_VERSION_OFFSET + FILE_VERSION_LENGTH)
6✔
221
    else {
222
        return Err(format!(
×
223
            "The buffer was too small to hold a VS_FIXEDFILEINFO struct: {data:X?}"
×
224
        ));
×
225
    };
226

227
    let file_minor = u16::from_le_bytes([*file_minor_0, *file_minor_1]);
6✔
228
    let file_major = u16::from_le_bytes([*file_major_0, *file_major_1]);
6✔
229
    let file_build = u16::from_le_bytes([*file_build_0, *file_build_1]);
6✔
230
    let file_patch = u16::from_le_bytes([*file_patch_0, *file_patch_1]);
6✔
231

232
    Ok(Version {
6✔
233
        release_ids: vec![
6✔
234
            ReleaseId::Numeric(u32::from(file_major)),
6✔
235
            ReleaseId::Numeric(u32::from(file_minor)),
6✔
236
            ReleaseId::Numeric(u32::from(file_patch)),
6✔
237
            ReleaseId::Numeric(u32::from(file_build)),
6✔
238
        ],
6✔
239
        pre_release_ids: Vec::new(),
6✔
240
    })
6✔
241
}
6✔
242

243
// <https://learn.microsoft.com/en-us/windows/win32/menurc/vs-versioninfo>
244
pub(super) fn read_product_version(data: &[u8]) -> Result<Option<Version>, String> {
10✔
245
    const CHILDREN_BASE_OFFSET: usize = 40;
246

247
    let StructHeaders {
248
        length,
10✔
249
        value_length,
10✔
250
    } = read_vs_version_info_headers(data)?;
10✔
251

252
    let mut children = subslice(
10✔
253
        data,
10✔
254
        CHILDREN_BASE_OFFSET + value_length,
10✔
255
        length - (CHILDREN_BASE_OFFSET + value_length),
10✔
256
    )?;
×
257

258
    while !children.is_empty() {
10✔
259
        let next_offset = match read_next_child(children)? {
10✔
260
            ReadResult::NewOffset(offset) => offset,
×
261
            ReadResult::Version(version) => return Ok(Some(Version::from(version))),
10✔
262
        };
263

264
        children = offset(children, next_offset)?;
×
265
    }
266

267
    Ok(None)
×
268
}
10✔
269

270
fn read_next_child(children: &[u8]) -> Result<ReadResult, String> {
10✔
271
    const STRING_FILE_INFO_KEY: &[u8; 30] = b"S\0t\0r\0i\0n\0g\0F\0i\0l\0e\0I\0n\0f\0o\0\0\0";
272

273
    let child_length = read_struct_size(children)?;
10✔
274

275
    if has_subslice_at(children, KEY_OFFSET, STRING_FILE_INFO_KEY) {
10✔
276
        // <https://learn.microsoft.com/en-us/windows/win32/menurc/stringfileinfo>
277
        const STRING_TABLES_OFFSET: usize = KEY_OFFSET + STRING_FILE_INFO_KEY.len();
278

279
        if child_length < STRING_TABLES_OFFSET {
10✔
UNCOV
280
            return Err(format!(
×
281
                "The StringFileInfo struct's header is too small: {child_length}"
×
282
            ));
×
283
        }
10✔
284

285
        let mut string_tables = subslice(
10✔
286
            children,
10✔
287
            STRING_TABLES_OFFSET,
288
            child_length - STRING_TABLES_OFFSET,
10✔
UNCOV
289
        )?;
×
290

291
        while !string_tables.is_empty() {
10✔
292
            let next_offset = match read_next_string_table(string_tables)? {
10✔
UNCOV
293
                ReadResult::NewOffset(offset) => offset,
×
294
                ReadResult::Version(version) => return Ok(ReadResult::Version(version)),
10✔
295
            };
296

UNCOV
297
            string_tables = offset(children, next_offset)?;
×
298
        }
UNCOV
299
    }
×
300

UNCOV
301
    Ok(ReadResult::NewOffset(new_aligned_offset(child_length)))
×
302
}
10✔
303

304
fn read_struct_size(buffer: &[u8]) -> Result<usize, String> {
20✔
305
    buffer
20✔
306
        .first_chunk::<2>()
20✔
307
        .map(|c| usize::from(u16::from_le_bytes(*c)))
20✔
308
        .ok_or_else(
20✔
UNCOV
309
            || format!("The buffer was too small to hold a struct size field: {buffer:X?}",),
×
310
        )
311
}
20✔
312

313
// <https://learn.microsoft.com/en-us/windows/win32/menurc/stringtable>
314
fn read_next_string_table(string_tables: &[u8]) -> Result<ReadResult, String> {
10✔
315
    const STRINGS_OFFSET: usize = 24;
316

317
    let string_table_length = read_struct_size(string_tables)?;
10✔
318

319
    if string_table_length < STRINGS_OFFSET {
10✔
UNCOV
320
        return Err(format!(
×
321
            "The StringTable struct's header is too small: {string_table_length}"
×
322
        ));
×
323
    }
10✔
324

325
    let mut strings = subslice(
10✔
326
        string_tables,
10✔
327
        STRINGS_OFFSET,
328
        string_table_length - STRINGS_OFFSET,
10✔
UNCOV
329
    )?;
×
330

331
    while !strings.is_empty() {
30✔
332
        let next_offset = match read_next_string(strings)? {
30✔
333
            ReadResult::NewOffset(offset) => offset,
20✔
334
            ReadResult::Version(version) => return Ok(ReadResult::Version(version)),
10✔
335
        };
336

337
        strings = offset(strings, next_offset)?;
20✔
338
    }
339

UNCOV
340
    Ok(ReadResult::NewOffset(new_aligned_offset(
×
341
        string_table_length,
×
342
    )))
×
343
}
10✔
344

345
// <https://learn.microsoft.com/en-us/windows/win32/menurc/string-str>
346
fn read_next_string(strings: &[u8]) -> Result<ReadResult, String> {
30✔
347
    const PRODUCT_VERSION_KEY: &[u8; 30] = b"P\0r\0o\0d\0u\0c\0t\0V\0e\0r\0s\0i\0o\0n\0\0\0";
348
    const VALUE_OFFSET: usize = KEY_OFFSET + PRODUCT_VERSION_KEY.len();
349

350
    let Ok(headers) = read_struct_headers(strings) else {
30✔
UNCOV
351
        return Err(format!(
×
352
            "The buffer was too small to hold a String struct: {strings:X?}"
×
353
        ));
×
354
    };
355

356
    if has_subslice_at(strings, KEY_OFFSET, PRODUCT_VERSION_KEY) {
30✔
357
        let string_bytes = subslice(strings, VALUE_OFFSET, headers.value_length * 2)?;
10✔
358
        let utf8_string = read_utf16_string(string_bytes).map_err(|e| e.to_string())?;
10✔
359
        return Ok(ReadResult::Version(utf8_string));
10✔
360
    }
20✔
361

362
    Ok(ReadResult::NewOffset(new_aligned_offset(headers.length)))
20✔
363
}
30✔
364

365
fn offset(bytes: &[u8], offset: usize) -> Result<&[u8], String> {
20✔
366
    bytes
20✔
367
        .get(offset..)
20✔
368
        .ok_or_else(|| format!("Failed to get subslice at offset {offset} of {bytes:X?}"))
20✔
369
}
20✔
370

371
fn new_aligned_offset(length_read: usize) -> usize {
20✔
372
    if length_read.is_multiple_of(4) {
20✔
UNCOV
373
        length_read
×
374
    } else {
375
        length_read + 2
20✔
376
    }
377
}
20✔
378

379
fn read_utf16_string(bytes: &[u8]) -> Result<String, std::string::FromUtf16Error> {
10✔
380
    // This could be made more efficient by checking alignment and transmuting
381
    // the slice if aligned, but that involves unsafe, and there isn't a
382
    // significant performance difference.
383
    let mut u16_vec: Vec<u16> = bytes
10✔
384
        .as_chunks::<2>()
10✔
385
        .0
10✔
386
        .iter()
10✔
387
        .map(|c| u16::from_le_bytes(*c))
70✔
388
        .collect();
10✔
389

390
    // We don't want to keep the trailing null u16.
391
    u16_vec.pop();
10✔
392

393
    String::from_utf16(&u16_vec)
10✔
394
}
10✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc