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

loot / loot-condition-interpreter / 19904365303

03 Dec 2025 06:11PM UTC coverage: 90.219% (+0.3%) from 89.921%
19904365303

push

github

Ortham
Remove pelite dependency

The advantages of using the built-in implementation instead of pelite are:

- It's much faster on average: for Starfield.exe (~ 100 MB) it is 2.8x
  faster and 4% slower reading file and product versions respectively,
  and for sfse_1_15_222.dll it is 3.35x faster and < 1% slower
  respectively.
- It reduces the transitive dependency count by 8
- It uses ~ 700 lines of first-party code that only depends on the
  standard library, instead of 240715 lines of unaudited third-party code
- pelite hasn't had a release in 3 years, and my PR for replacing winapi
  hasn't had any response in the month and a half it's been open, so the
  built-in implementation is probably less of a maintenance risk.

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

59 existing lines in 2 files now uncovered.

4492 of 4979 relevant lines covered (90.22%)

31.57 hits per line

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

85.28
/src/function/version/pe.rs
1
use std::{
2
    fs::File,
3
    io::{BufReader, Read, Seek, SeekFrom},
4
    path::Path,
5
};
6

7
use crate::Error;
8

9
use super::{ReleaseId, Version};
10

11
const KEY_OFFSET: usize = 6;
12

13
struct StructHeaders {
14
    length: usize,
15
    value_length: usize,
16
}
17

18
enum ReadResult {
19
    Version(String),
20
    NewOffset(usize),
21
}
22

23
pub(super) fn read_pe_version<F>(
40✔
24
    file_path: &Path,
40✔
25
    read_from_version_info: F,
40✔
26
) -> Result<Option<Version>, Error>
40✔
27
where
40✔
28
    F: Fn(&[u8]) -> Result<Option<Version>, String>,
40✔
29
{
30
    let file = File::open(file_path).map_err(|e| Error::IoError(file_path.to_path_buf(), e))?;
40✔
31
    let mut reader = BufReader::new(file);
31✔
32

33
    let data = read_version_resource_data(&mut reader)
31✔
34
        .map_err(|e| Error::PeParsingError(file_path.to_path_buf(), e.into()))?;
31✔
35

36
    if let Some(data) = data {
22✔
37
        read_from_version_info(&data)
18✔
38
            .map_err(|e| Error::PeParsingError(file_path.to_path_buf(), e.into()))
18✔
39
    } else {
40
        Ok(None)
4✔
41
    }
42
}
40✔
43

44
// <https://coffi.readthedocs.io/en/latest/pecoff_v11.pdf>
45
// <https://0xrick.github.io/win-internals/pe3/>
46
fn read_version_resource_data<T: Read + Seek>(reader: &mut T) -> std::io::Result<Option<Vec<u8>>> {
31✔
47
    const DOS_MAGIC: &[u8; 2] = b"MZ";
48
    const PE_MAGIC: &[u8; 4] = b"PE\0\0";
49
    const PE_HEADER_OFFSET_OFFSET: u64 = 0x3C;
50

51
    let mut dos_magic = [0u8; 2];
31✔
52
    reader.read_exact(&mut dos_magic)?;
31✔
53

54
    if &dos_magic != DOS_MAGIC {
30✔
55
        return Err(std::io::Error::other("Unknown file magic"));
8✔
56
    }
22✔
57

58
    reader.seek(std::io::SeekFrom::Start(PE_HEADER_OFFSET_OFFSET))?;
22✔
59

60
    let mut pe_header_offset = [0u8; 2];
22✔
61
    reader.read_exact(&mut pe_header_offset)?;
22✔
62

63
    let pe_header_offset = u64::from(u16::from_le_bytes(pe_header_offset));
22✔
64

65
    reader.seek(std::io::SeekFrom::Start(pe_header_offset))?;
22✔
66

67
    let mut pe_magic = [0u8; 4];
22✔
68
    reader.read_exact(&mut pe_magic)?;
22✔
69

70
    if &pe_magic != PE_MAGIC {
22✔
UNCOV
71
        return Err(std::io::Error::other("Unexpected PE header magic bytes"));
×
72
    }
22✔
73

74
    let coff_header = CoffFileHeader::read(reader)?;
22✔
75

76
    let optional_header =
22✔
77
        OptionalHeader::read(reader, u64::from(coff_header.optional_header_size))?;
22✔
78

79
    let Some(resource_table_data_directory) = optional_header.resource_table_data_directory()
22✔
80
    else {
UNCOV
81
        return Ok(None);
×
82
    };
83

84
    for _ in 0..coff_header.number_of_sections {
22✔
85
        let entry = SectionTableEntry::read(reader)?;
146✔
86

87
        if let Some(table_data) = resource_table_data_directory.read_data(reader, &entry)? {
146✔
88
            return read_version_data(&entry, &table_data);
22✔
89
        }
124✔
90
    }
91

UNCOV
92
    Ok(None)
×
93
}
31✔
94

95
fn read_version_data(
22✔
96
    table_entry: &SectionTableEntry,
22✔
97
    resource_table_data: &[u8],
22✔
98
) -> std::io::Result<Option<Vec<u8>>> {
22✔
99
    // The table entry offsets within the resource section are relative to the
100
    // start of the section, so it's easier to read the tables and entries from
101
    // a buffer that only contains the section.
102
    let mut cursor = std::io::Cursor::new(&resource_table_data);
22✔
103

104
    let Some(version_data_entry) = read_resource_tables(&mut cursor)? else {
22✔
105
        return Ok(None);
4✔
106
    };
107

108
    // However, the version data's offset is given relative to the start of the
109
    // loaded executable's virtual address.
110
    let data_offset = version_data_entry.data_rva - table_entry.virtual_address;
18✔
111

112
    cursor.seek(SeekFrom::Start(u64::from(data_offset)))?;
18✔
113

114
    let mut version_data = vec![0; to_usize(version_data_entry.size)];
18✔
115
    cursor.read_exact(&mut version_data)?;
18✔
116

117
    Ok(Some(version_data))
18✔
118
}
22✔
119

120
/// This expects to be given a reader that's at the start of the resource table
121
/// data.
122
fn read_resource_tables<T: Read + Seek>(
22✔
123
    reader: &mut T,
22✔
124
) -> std::io::Result<Option<ResourceDataEntry>> {
22✔
125
    let root_table = ResourceDirectoryTable::read(reader)?;
22✔
126

127
    for root_entry in root_table.entries {
26✔
128
        if root_entry.name_offset_or_id == ResourceDirectoryEntry::RT_VERSION
22✔
129
            && root_entry.is_table()
18✔
130
        {
131
            reader.seek(SeekFrom::Start(u64::from(root_entry.offset())))?;
18✔
132

133
            let version_name_table = ResourceDirectoryTable::read(reader)?;
18✔
134

135
            for name_entry in version_name_table.entries {
18✔
136
                if name_entry.is_table() {
18✔
137
                    reader.seek(SeekFrom::Start(u64::from(name_entry.offset())))?;
18✔
138

139
                    let version_language_table = ResourceDirectoryTable::read(reader)?;
18✔
140

141
                    for language_entry in version_language_table.entries {
18✔
142
                        if !language_entry.is_table() {
18✔
143
                            reader.seek(SeekFrom::Start(u64::from(language_entry.offset())))?;
18✔
144

145
                            return ResourceDataEntry::read(reader).map(Some);
18✔
UNCOV
146
                        }
×
147
                    }
UNCOV
148
                }
×
149
            }
150
        }
4✔
151
    }
152

153
    Ok(None)
4✔
154
}
22✔
155

156
#[derive(Debug)]
157
struct CoffFileHeader {
158
    number_of_sections: u16,
159
    optional_header_size: u16,
160
}
161

162
impl CoffFileHeader {
163
    fn read<T: Read + Seek>(reader: &mut T) -> std::io::Result<Self> {
22✔
164
        let mut word = [0u8; 2];
22✔
165

166
        // Skip machine.
167
        reader.seek_relative(2)?;
22✔
168

169
        reader.read_exact(&mut word)?;
22✔
170
        let number_of_sections = u16::from_le_bytes(word);
22✔
171

172
        // Skip time_date_stamp, symbol_table_offset and number_of_symbols.
173
        reader.seek_relative(12)?;
22✔
174

175
        reader.read_exact(&mut word)?;
22✔
176
        let optional_header_size = u16::from_le_bytes(word);
22✔
177

178
        // Optional header is required for executables.
179
        if optional_header_size == 0 {
22✔
UNCOV
180
            return Err(std::io::Error::other(
×
UNCOV
181
                "COFF optional header is unexpectedly missing",
×
UNCOV
182
            ));
×
183
        }
22✔
184

185
        // Skip characteristics.
186
        reader.seek_relative(2)?;
22✔
187

188
        Ok(Self {
22✔
189
            number_of_sections,
22✔
190
            optional_header_size,
22✔
191
        })
22✔
192
    }
22✔
193
}
194

195
#[derive(Debug)]
196
struct OptionalHeader {
197
    image_data_directories: Vec<ImageDataDirectory>,
198
}
199

200
impl OptionalHeader {
201
    const PE32_MAGIC: u16 = 0x10b;
202
    const RESOURCE_TABLE_DATA_DIRECTORY_OFFSET: usize = 2;
203

204
    /// Ensure that reading the optional header is restricted to the declared
205
    /// size of the header, since otherwise an invalid number_of_rva_and_sizes
206
    /// field value could cause the reader to go past the declared end of the
207
    /// header.
208
    fn read<T: Read + Seek>(reader: &mut T, header_size: u64) -> std::io::Result<Self> {
22✔
209
        let reader = &mut reader.take(header_size);
22✔
210
        let mut word = [0u8; 2];
22✔
211

212
        reader.read_exact(&mut word)?;
22✔
213
        let magic = u16::from_le_bytes(word);
22✔
214

215
        // Skip many fields.
216
        if magic == Self::PE32_MAGIC {
22✔
217
            reader.seek_relative(90)?;
18✔
218
        } else {
219
            reader.seek_relative(106)?;
4✔
220
        }
221

222
        let mut dword = [0u8; 4];
22✔
223

224
        reader.read_exact(&mut dword)?;
22✔
225
        let number_of_rva_and_sizes = u32::from_le_bytes(dword);
22✔
226

227
        let mut image_data_directories = Vec::with_capacity(to_usize(number_of_rva_and_sizes));
22✔
228
        for _ in 0..number_of_rva_and_sizes {
22✔
229
            image_data_directories.push(ImageDataDirectory::read(reader)?);
352✔
230
        }
231

232
        Ok(OptionalHeader {
22✔
233
            image_data_directories,
22✔
234
        })
22✔
235
    }
22✔
236

237
    fn resource_table_data_directory(&self) -> Option<&ImageDataDirectory> {
22✔
238
        self.image_data_directories
22✔
239
            .get(OptionalHeader::RESOURCE_TABLE_DATA_DIRECTORY_OFFSET)
22✔
240
    }
22✔
241
}
242

243
#[derive(Debug)]
244
struct ImageDataDirectory {
245
    virtual_address: u32,
246
    size: u32,
247
}
248

249
impl ImageDataDirectory {
250
    fn read<T: Read>(reader: &mut T) -> std::io::Result<Self> {
352✔
251
        let mut dword = [0u8; 4];
352✔
252

253
        reader.read_exact(&mut dword)?;
352✔
254
        let virtual_address = u32::from_le_bytes(dword);
352✔
255

256
        reader.read_exact(&mut dword)?;
352✔
257
        let size = u32::from_le_bytes(dword);
352✔
258

259
        Ok(Self {
352✔
260
            virtual_address,
352✔
261
            size,
352✔
262
        })
352✔
263
    }
352✔
264

265
    fn read_data<T: Read + Seek>(
146✔
266
        &self,
146✔
267
        reader: &mut T,
146✔
268
        entry: &SectionTableEntry,
146✔
269
    ) -> std::io::Result<Option<Vec<u8>>> {
146✔
270
        if entry.contains(self) {
146✔
271
            let table_offset = self.virtual_address - entry.virtual_address;
22✔
272

273
            reader.seek(std::io::SeekFrom::Start(u64::from(
22✔
274
                entry.raw_data_offset + table_offset,
22✔
275
            )))?;
22✔
276

277
            let mut data = vec![0; to_usize(self.size)];
22✔
278
            reader.read_exact(&mut data)?;
22✔
279

280
            Ok(Some(data))
22✔
281
        } else {
282
            Ok(None)
124✔
283
        }
284
    }
146✔
285
}
286

287
#[derive(Debug)]
288
struct SectionTableEntry {
289
    virtual_size: u32,
290
    virtual_address: u32,
291
    raw_data_size: u32,
292
    raw_data_offset: u32,
293
}
294

295
impl SectionTableEntry {
296
    fn read<T: Read + Seek>(reader: &mut T) -> std::io::Result<Self> {
146✔
297
        let mut name = [0u8; 8];
146✔
298
        reader.read_exact(&mut name)?;
146✔
299

300
        let mut dword = [0u8; 4];
146✔
301

302
        reader.read_exact(&mut dword)?;
146✔
303
        let virtual_size = u32::from_le_bytes(dword);
146✔
304

305
        reader.read_exact(&mut dword)?;
146✔
306
        let virtual_address = u32::from_le_bytes(dword);
146✔
307

308
        reader.read_exact(&mut dword)?;
146✔
309
        let raw_data_size = u32::from_le_bytes(dword);
146✔
310

311
        reader.read_exact(&mut dword)?;
146✔
312
        let raw_data_offset = u32::from_le_bytes(dword);
146✔
313

314
        // Skip relocations_offset, line_numbers_offset, relocations_count,
315
        // line_numbers_count and characteristics.
316
        reader.seek_relative(16)?;
146✔
317

318
        Ok(Self {
146✔
319
            virtual_size,
146✔
320
            virtual_address,
146✔
321
            raw_data_size,
146✔
322
            raw_data_offset,
146✔
323
        })
146✔
324
    }
146✔
325

326
    fn contains(&self, image_data_directory: &ImageDataDirectory) -> bool {
146✔
327
        let section_end = self.virtual_address + self.actual_size();
146✔
328
        let directory_end = image_data_directory.virtual_address + image_data_directory.size;
146✔
329

330
        image_data_directory.virtual_address >= self.virtual_address && directory_end <= section_end
146✔
331
    }
146✔
332

333
    fn actual_size(&self) -> u32 {
146✔
334
        std::cmp::min(self.raw_data_size, self.virtual_size)
146✔
335
    }
146✔
336
}
337

338
#[derive(Debug)]
339
struct ResourceDirectoryTable {
340
    entries: Vec<ResourceDirectoryEntry>,
341
}
342

343
impl ResourceDirectoryTable {
344
    fn read<T: Read + Seek>(reader: &mut T) -> std::io::Result<Self> {
58✔
345
        // Skip characteristics, time_date_stamp, major_version and minor_version.
346
        reader.seek_relative(12)?;
58✔
347

348
        let mut word = [0u8; 2];
58✔
349

350
        reader.read_exact(&mut word)?;
58✔
351
        let name_entry_count = u16::from_le_bytes(word);
58✔
352

353
        reader.read_exact(&mut word)?;
58✔
354
        let id_entry_count = u16::from_le_bytes(word);
58✔
355

356
        let mut entries = Vec::with_capacity(usize::from(name_entry_count + id_entry_count));
58✔
357
        for _ in 0..name_entry_count {
58✔
UNCOV
358
            entries.push(ResourceDirectoryEntry::read(reader)?);
×
359
        }
360

361
        for _ in 0..id_entry_count {
58✔
362
            entries.push(ResourceDirectoryEntry::read(reader)?);
76✔
363
        }
364

365
        Ok(Self { entries })
58✔
366
    }
58✔
367
}
368

369
#[derive(Debug)]
370
struct ResourceDirectoryEntry {
371
    name_offset_or_id: u32,
372
    data_entry_or_subdirectory_offset: u32,
373
}
374

375
impl ResourceDirectoryEntry {
376
    const RT_VERSION: u32 = 16;
377

378
    fn read<T: Read>(reader: &mut T) -> std::io::Result<Self> {
76✔
379
        let mut dword = [0u8; 4];
76✔
380

381
        reader.read_exact(&mut dword)?;
76✔
382
        let name_offset_or_id = u32::from_le_bytes(dword);
76✔
383

384
        reader.read_exact(&mut dword)?;
76✔
385
        let data_entry_or_subdirectory_offset = u32::from_le_bytes(dword);
76✔
386

387
        Ok(Self {
76✔
388
            name_offset_or_id,
76✔
389
            data_entry_or_subdirectory_offset,
76✔
390
        })
76✔
391
    }
76✔
392

393
    fn is_table(&self) -> bool {
108✔
394
        (self.data_entry_or_subdirectory_offset & (1 << 31)) != 0
108✔
395
    }
108✔
396

397
    fn offset(&self) -> u32 {
54✔
398
        if self.is_table() {
54✔
399
            self.data_entry_or_subdirectory_offset ^ (1 << 31)
36✔
400
        } else {
401
            self.data_entry_or_subdirectory_offset
18✔
402
        }
403
    }
54✔
404
}
405

406
#[derive(Debug)]
407
struct ResourceDataEntry {
408
    data_rva: u32,
409
    size: u32,
410
}
411

412
impl ResourceDataEntry {
413
    fn read<T: Read + Seek>(reader: &mut T) -> std::io::Result<Self> {
18✔
414
        let mut dword = [0u8; 4];
18✔
415

416
        reader.read_exact(&mut dword)?;
18✔
417
        let data_rva = u32::from_le_bytes(dword);
18✔
418

419
        reader.read_exact(&mut dword)?;
18✔
420
        let size = u32::from_le_bytes(dword);
18✔
421

422
        // Skip codepage and reserved.
423
        reader.seek_relative(8)?;
18✔
424

425
        Ok(Self { data_rva, size })
18✔
426
    }
18✔
427
}
428

429
#[expect(
430
    clippy::as_conversions,
431
    reason = "A compile-time assertion ensures that this conversion will be lossless on all relevant target platforms"
432
)]
433
const fn to_usize(value: u32) -> usize {
62✔
434
    // Error at compile time if this conversion isn't lossless.
435
    const _: () = assert!(u32::BITS <= usize::BITS, "cannot fit a u32 into a usize!");
436
    value as usize
62✔
437
}
62✔
438

439
fn subslice(bytes: &[u8], offset: usize, size: usize) -> Result<&[u8], String> {
46✔
440
    bytes.get(offset..offset + size).ok_or_else(|| {
46✔
UNCOV
441
        format!("Invalid subslice of size {size} at offset {offset} of bytes {bytes:X?}")
×
UNCOV
442
    })
×
443
}
46✔
444

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

449
    if value_length == 0 {
6✔
UNCOV
450
        Ok(None)
×
451
    } else {
452
        let fixed_file_info = subslice(data, 40, value_length)?;
6✔
453
        Some(read_vs_fixed_file_info(fixed_file_info)).transpose()
6✔
454
    }
455
}
6✔
456

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

460
    if headers.length != data.len() {
16✔
UNCOV
461
        return Err(format!(
×
UNCOV
462
            "Unexpected length of VS_VERSIONINFO struct, got {} but buffer length is {}",
×
UNCOV
463
            headers.length,
×
UNCOV
464
            data.len()
×
UNCOV
465
        ));
×
466
    }
16✔
467

468
    if !has_subslice_at(
16✔
469
        data,
16✔
470
        KEY_OFFSET,
16✔
471
        b"V\0S\0_\0V\0E\0R\0S\0I\0O\0N\0_\0I\0N\0F\0O\0\0\0",
16✔
472
    ) {
16✔
UNCOV
473
        return Err(format!(
×
UNCOV
474
            "The szKey field's value is not valid for a VS_VERSIONINFO struct: {data:X?}"
×
UNCOV
475
        ));
×
476
    }
16✔
477

478
    Ok(headers)
16✔
479
}
16✔
480

481
fn read_struct_headers(data: &[u8]) -> Result<StructHeaders, String> {
46✔
482
    let [l0, l1, vl0, vl1, ..] = data else {
46✔
UNCOV
483
        return Err(format!(
×
UNCOV
484
            "The buffer was too small to hold a struct: {data:X?}"
×
UNCOV
485
        ));
×
486
    };
487

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

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

492
    Ok(StructHeaders {
46✔
493
        length,
46✔
494
        value_length,
46✔
495
    })
46✔
496
}
46✔
497

498
fn has_subslice_at(haystack: &[u8], offset: usize, needle: &[u8]) -> bool {
62✔
499
    haystack
62✔
500
        .get(offset..offset + needle.len())
62✔
501
        .is_some_and(|s| s == needle)
62✔
502
}
62✔
503

504
// <learn.microsoft.com/en-us/windows/win32/api/verrsrc/ns-verrsrc-vs_fixedfileinfo>
505
fn read_vs_fixed_file_info(data: &[u8]) -> Result<Version, String> {
6✔
506
    const VS_FIXEDFILEINFO_SIZE: usize = 0x34;
507
    const VS_FIXEDFILEINFO_SIGNATURE: [u8; 4] = [0xBD, 0x04, 0xEF, 0xFE];
508
    const FILE_VERSION_OFFSET: usize = 8;
509
    const FILE_VERSION_LENGTH: usize = 8;
510

511
    if data.len() != VS_FIXEDFILEINFO_SIZE {
6✔
UNCOV
512
        return Err(format!(
×
UNCOV
513
            "Unexpected length of VS_VERSIONINFO value, got {} but expected {}",
×
UNCOV
514
            data.len(),
×
UNCOV
515
            VS_FIXEDFILEINFO_SIZE
×
UNCOV
516
        ));
×
517
    }
6✔
518

519
    if !has_subslice_at(data, 0, &VS_FIXEDFILEINFO_SIGNATURE) {
6✔
UNCOV
520
        return Err(format!(
×
UNCOV
521
            "Unexpected first four bytes of VS_VERSIONINFO struct {data:X?}"
×
UNCOV
522
        ));
×
523
    }
6✔
524

525
    let Some(
526
        [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✔
527
    ) = data.get(FILE_VERSION_OFFSET..FILE_VERSION_OFFSET + FILE_VERSION_LENGTH)
6✔
528
    else {
UNCOV
529
        return Err(format!(
×
UNCOV
530
            "The buffer was too small to hold a VS_FIXEDFILEINFO struct: {data:X?}"
×
UNCOV
531
        ));
×
532
    };
533

534
    let file_minor = u16::from_le_bytes([*file_minor_0, *file_minor_1]);
6✔
535
    let file_major = u16::from_le_bytes([*file_major_0, *file_major_1]);
6✔
536
    let file_build = u16::from_le_bytes([*file_build_0, *file_build_1]);
6✔
537
    let file_patch = u16::from_le_bytes([*file_patch_0, *file_patch_1]);
6✔
538

539
    Ok(Version {
6✔
540
        release_ids: vec![
6✔
541
            ReleaseId::Numeric(u32::from(file_major)),
6✔
542
            ReleaseId::Numeric(u32::from(file_minor)),
6✔
543
            ReleaseId::Numeric(u32::from(file_patch)),
6✔
544
            ReleaseId::Numeric(u32::from(file_build)),
6✔
545
        ],
6✔
546
        pre_release_ids: Vec::new(),
6✔
547
    })
6✔
548
}
6✔
549

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

554
    let StructHeaders {
555
        length,
10✔
556
        value_length,
10✔
557
    } = read_vs_version_info_headers(data)?;
10✔
558

559
    let mut children = subslice(
10✔
560
        data,
10✔
561
        CHILDREN_BASE_OFFSET + value_length,
10✔
562
        length - (CHILDREN_BASE_OFFSET + value_length),
10✔
UNCOV
563
    )?;
×
564

565
    while !children.is_empty() {
10✔
566
        let next_offset = match read_next_child(children)? {
10✔
UNCOV
567
            ReadResult::NewOffset(offset) => offset,
×
568
            ReadResult::Version(version) => return Ok(Some(Version::from(version))),
10✔
569
        };
570

UNCOV
571
        children = offset(children, next_offset)?;
×
572
    }
573

UNCOV
574
    Ok(None)
×
575
}
10✔
576

577
fn read_next_child(children: &[u8]) -> Result<ReadResult, String> {
10✔
578
    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";
579

580
    let child_length = read_struct_size(children)?;
10✔
581

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

586
        if child_length < STRING_TABLES_OFFSET {
10✔
UNCOV
587
            return Err(format!(
×
UNCOV
588
                "The StringFileInfo struct's header is too small: {child_length}"
×
UNCOV
589
            ));
×
590
        }
10✔
591

592
        let mut string_tables = subslice(
10✔
593
            children,
10✔
594
            STRING_TABLES_OFFSET,
595
            child_length - STRING_TABLES_OFFSET,
10✔
UNCOV
596
        )?;
×
597

598
        while !string_tables.is_empty() {
10✔
599
            let next_offset = match read_next_string_table(string_tables)? {
10✔
UNCOV
600
                ReadResult::NewOffset(offset) => offset,
×
601
                ReadResult::Version(version) => return Ok(ReadResult::Version(version)),
10✔
602
            };
603

UNCOV
604
            string_tables = offset(children, next_offset)?;
×
605
        }
UNCOV
606
    }
×
607

UNCOV
608
    Ok(ReadResult::NewOffset(new_aligned_offset(child_length)))
×
609
}
10✔
610

611
fn read_struct_size(buffer: &[u8]) -> Result<usize, String> {
20✔
612
    buffer
20✔
613
        .first_chunk::<2>()
20✔
614
        .map(|c| usize::from(u16::from_le_bytes(*c)))
20✔
615
        .ok_or_else(
20✔
UNCOV
616
            || format!("The buffer was too small to hold a struct size field: {buffer:X?}",),
×
617
        )
618
}
20✔
619

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

624
    let string_table_length = read_struct_size(string_tables)?;
10✔
625

626
    if string_table_length < STRINGS_OFFSET {
10✔
UNCOV
627
        return Err(format!(
×
UNCOV
628
            "The StringTable struct's header is too small: {string_table_length}"
×
UNCOV
629
        ));
×
630
    }
10✔
631

632
    let mut strings = subslice(
10✔
633
        string_tables,
10✔
634
        STRINGS_OFFSET,
635
        string_table_length - STRINGS_OFFSET,
10✔
UNCOV
636
    )?;
×
637

638
    while !strings.is_empty() {
30✔
639
        let next_offset = match read_next_string(strings)? {
30✔
640
            ReadResult::NewOffset(offset) => offset,
20✔
641
            ReadResult::Version(version) => return Ok(ReadResult::Version(version)),
10✔
642
        };
643

644
        strings = offset(strings, next_offset)?;
20✔
645
    }
646

UNCOV
647
    Ok(ReadResult::NewOffset(new_aligned_offset(
×
UNCOV
648
        string_table_length,
×
UNCOV
649
    )))
×
650
}
10✔
651

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

657
    let Ok(headers) = read_struct_headers(strings) else {
30✔
UNCOV
658
        return Err(format!(
×
UNCOV
659
            "The buffer was too small to hold a String struct: {strings:X?}"
×
UNCOV
660
        ));
×
661
    };
662

663
    if has_subslice_at(strings, KEY_OFFSET, PRODUCT_VERSION_KEY) {
30✔
664
        let string_bytes = subslice(strings, VALUE_OFFSET, headers.value_length * 2)?;
10✔
665
        let utf8_string = read_utf16_string(string_bytes).map_err(|e| e.to_string())?;
10✔
666
        return Ok(ReadResult::Version(utf8_string));
10✔
667
    }
20✔
668

669
    Ok(ReadResult::NewOffset(new_aligned_offset(headers.length)))
20✔
670
}
30✔
671

672
fn offset(bytes: &[u8], offset: usize) -> Result<&[u8], String> {
20✔
673
    bytes
20✔
674
        .get(offset..)
20✔
675
        .ok_or_else(|| format!("Failed to get subslice at offset {offset} of {bytes:X?}"))
20✔
676
}
20✔
677

678
fn new_aligned_offset(length_read: usize) -> usize {
20✔
679
    if length_read.is_multiple_of(4) {
20✔
UNCOV
680
        length_read
×
681
    } else {
682
        length_read + 2
20✔
683
    }
684
}
20✔
685

686
fn read_utf16_string(bytes: &[u8]) -> Result<String, std::string::FromUtf16Error> {
10✔
687
    // This could be made more efficient by checking alignment and transmuting
688
    // the slice if aligned, but that involves unsafe, and there isn't a
689
    // significant performance difference.
690
    let mut u16_vec: Vec<u16> = bytes
10✔
691
        .as_chunks::<2>()
10✔
692
        .0
10✔
693
        .iter()
10✔
694
        .map(|c| u16::from_le_bytes(*c))
70✔
695
        .collect();
10✔
696

697
    // We don't want to keep the trailing null u16.
698
    u16_vec.pop();
10✔
699

700
    String::from_utf16(&u16_vec)
10✔
701
}
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