Coverage Report

Created: 2026-01-25 07:18

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/kio-extras/thumbnail/exeutils.cpp
Line
Count
Source
1
/*
2
    exeutils.cpp - Extract Microsoft Window icons from Microsoft Windows executables
3
4
    SPDX-FileCopyrightText: 2023 John Chadwick <john@jchw.io>
5
6
    SPDX-License-Identifier: LGPL-2.0-or-later OR BSD-2-Clause
7
*/
8
9
#include "exeutils.h"
10
11
#include <QDataStream>
12
#include <QIODevice>
13
#include <QMap>
14
#include <QVector>
15
16
#include <optional>
17
18
namespace
19
{
20
21
// Executable file (.exe)
22
struct DosHeader {
23
    char signature[2];
24
    quint32 newHeaderOffset;
25
};
26
27
QDataStream &operator>>(QDataStream &s, DosHeader &v)
28
3.69k
{
29
3.69k
    s.readRawData(v.signature, sizeof(v.signature));
30
3.69k
    s.device()->skip(58);
31
3.69k
    s >> v.newHeaderOffset;
32
3.69k
    return s;
33
3.69k
}
34
35
// Resource Directory
36
enum class ResourceType : quint32 {
37
    Icon = 3,
38
    GroupIcon = 14,
39
};
40
41
struct RtGroupIconDirectory {
42
    quint16 reserved;
43
    quint16 type;
44
    quint16 count;
45
};
46
47
struct RtGroupIconDirectoryEntry {
48
    quint8 width;
49
    quint8 height;
50
    quint8 colorCount;
51
    quint8 reserved;
52
    quint16 numPlanes;
53
    quint16 bpp;
54
    quint32 size;
55
    quint16 resourceId;
56
};
57
58
QDataStream &operator>>(QDataStream &s, RtGroupIconDirectory &v)
59
1.64k
{
60
1.64k
    s >> v.reserved >> v.type >> v.count;
61
1.64k
    return s;
62
1.64k
}
63
64
QDataStream &operator>>(QDataStream &s, RtGroupIconDirectoryEntry &v)
65
3.51M
{
66
3.51M
    s >> v.width >> v.height >> v.colorCount >> v.reserved >> v.numPlanes >> v.bpp >> v.size >> v.resourceId;
67
3.51M
    return s;
68
3.51M
}
69
70
// Icon file (.ico)
71
struct IconDir {
72
    quint16 reserved;
73
    quint16 type;
74
    quint16 count;
75
};
76
77
constexpr int IconDirSize = 6;
78
79
struct IconDirEntry {
80
    quint8 width;
81
    quint8 height;
82
    quint8 colorCount;
83
    quint8 reserved;
84
    quint16 numPlanes;
85
    quint16 bpp;
86
    quint32 size;
87
    quint32 imageOffset;
88
89
    IconDirEntry(const RtGroupIconDirectoryEntry &entry, quint32 dataOffset)
90
3.51M
    {
91
3.51M
        width = entry.width;
92
3.51M
        height = entry.height;
93
3.51M
        colorCount = entry.colorCount;
94
3.51M
        reserved = entry.reserved;
95
3.51M
        numPlanes = entry.numPlanes;
96
3.51M
        bpp = entry.bpp;
97
3.51M
        size = entry.size;
98
3.51M
        imageOffset = dataOffset;
99
3.51M
    }
100
};
101
102
constexpr int IconDirEntrySize = 16;
103
104
QDataStream &operator<<(QDataStream &s, const IconDir &v)
105
1.64k
{
106
1.64k
    s << v.reserved << v.type << v.count;
107
1.64k
    return s;
108
1.64k
}
109
110
QDataStream &operator<<(QDataStream &s, const IconDirEntry &v)
111
3.51M
{
112
3.51M
    s << v.width << v.height << v.colorCount << v.reserved << v.numPlanes << v.bpp << v.size << v.imageOffset;
113
3.51M
    return s;
114
3.51M
}
115
116
// Win16 New Executable
117
118
struct NeFileHeader {
119
    quint16 offsetOfResourceTable;
120
    quint16 numberOfResourceSegments;
121
};
122
123
struct NeResource {
124
    quint16 dataOffsetShifted;
125
    quint16 dataLength;
126
    quint16 flags;
127
    quint16 resourceId;
128
    quint16 resource[2];
129
};
130
131
struct NeResourceTable {
132
    struct Type {
133
        quint16 typeId;
134
        quint16 numResources;
135
        quint16 resource[2];
136
        QVector<NeResource> resources;
137
    };
138
139
    quint16 alignmentShiftCount;
140
    QMap<ResourceType, Type> types;
141
};
142
143
QDataStream &operator>>(QDataStream &s, NeFileHeader &v)
144
1.65k
{
145
1.65k
    s.device()->skip(34);
146
1.65k
    s >> v.offsetOfResourceTable;
147
1.65k
    s.device()->skip(14);
148
1.65k
    s >> v.numberOfResourceSegments;
149
1.65k
    return s;
150
1.65k
}
151
152
QDataStream &operator>>(QDataStream &s, NeResource &v)
153
11.3M
{
154
11.3M
    s >> v.dataOffsetShifted >> v.dataLength >> v.flags >> v.resourceId >> v.resource[0] >> v.resource[1];
155
11.3M
    v.resourceId ^= 0x8000;
156
11.3M
    return s;
157
11.3M
}
158
159
QDataStream &operator>>(QDataStream &s, NeResourceTable::Type &v)
160
10.5k
{
161
10.5k
    s >> v.typeId;
162
10.5k
    if (v.typeId == 0) {
163
1.65k
        return s;
164
1.65k
    }
165
8.92k
    s >> v.numResources >> v.resource[0] >> v.resource[1];
166
11.3M
    for (int i = 0; i < v.numResources; i++) {
167
11.3M
        NeResource resource;
168
11.3M
        s >> resource;
169
11.3M
        v.resources.append(resource);
170
11.3M
    }
171
8.92k
    return s;
172
10.5k
}
173
174
QDataStream &operator>>(QDataStream &s, NeResourceTable &v)
175
1.65k
{
176
1.65k
    s >> v.alignmentShiftCount;
177
10.5k
    while (1) {
178
10.5k
        NeResourceTable::Type type;
179
10.5k
        s >> type;
180
10.5k
        if (!type.typeId) {
181
1.65k
            break;
182
1.65k
        }
183
8.92k
        v.types[ResourceType(type.typeId ^ 0x8000)] = type;
184
8.92k
    }
185
1.65k
    return s;
186
1.65k
}
187
188
bool readNewExecutablePrimaryIcon(QDataStream &ds, const DosHeader &dosHeader, QIODevice *outputDevice)
189
3.09k
{
190
3.09k
    NeFileHeader fileHeader;
191
3.09k
    NeResourceTable resources;
192
3.09k
    QDataStream out{outputDevice};
193
194
3.09k
    out.setByteOrder(QDataStream::LittleEndian);
195
196
3.09k
    if (!ds.device()->seek(dosHeader.newHeaderOffset)) {
197
0
        return false;
198
0
    }
199
200
3.09k
    char signature[2];
201
3.09k
    ds.readRawData(signature, sizeof(signature));
202
203
3.09k
    if (signature[0] != 'N' || signature[1] != 'E') {
204
1.43k
        return false;
205
1.43k
    }
206
207
1.65k
    ds >> fileHeader;
208
1.65k
    if (!ds.device()->seek(dosHeader.newHeaderOffset + fileHeader.offsetOfResourceTable)) {
209
0
        return false;
210
0
    }
211
212
1.65k
    ds >> resources;
213
214
1.65k
    if (!resources.types.contains(ResourceType::GroupIcon) || !resources.types.contains(ResourceType::Icon)) {
215
242
        return false;
216
242
    }
217
218
1.41k
    auto iconResources = resources.types[ResourceType::Icon].resources;
219
220
1.41k
    auto iconGroupResources = resources.types[ResourceType::GroupIcon];
221
1.41k
    if (iconGroupResources.resources.empty()) {
222
10
        return false;
223
10
    }
224
225
1.40k
    auto iconGroupResource = iconGroupResources.resources.first();
226
1.40k
    if (!ds.device()->seek(iconGroupResource.dataOffsetShifted << resources.alignmentShiftCount)) {
227
11
        return false;
228
11
    }
229
230
1.39k
    RtGroupIconDirectory iconGroup;
231
1.39k
    ds >> iconGroup;
232
233
1.39k
    IconDir icoHeader;
234
1.39k
    icoHeader.reserved = 0;
235
1.39k
    icoHeader.type = 1; // Always 1 for ico files.
236
1.39k
    icoHeader.count = iconGroup.count;
237
1.39k
    out << icoHeader;
238
239
1.39k
    quint32 dataOffset = IconDirSize + IconDirEntrySize * iconGroup.count;
240
1.39k
    QVector<QPair<qint64, quint32>> resourceOffsetSizePairs;
241
242
3.43M
    for (int i = 0; i < iconGroup.count; i++) {
243
3.43M
        RtGroupIconDirectoryEntry entry;
244
3.43M
        ds >> entry;
245
246
3.45M
        auto it = std::find_if(iconResources.begin(), iconResources.end(), [&entry](const NeResource &res) {
247
3.45M
            return res.resourceId == entry.resourceId;
248
3.45M
        });
249
3.43M
        if (it == iconResources.end()) {
250
178
            return false;
251
178
        }
252
253
3.43M
        IconDirEntry icoEntry{entry, dataOffset};
254
3.43M
        out << icoEntry;
255
256
3.43M
        NeResource iconRes = *it;
257
3.43M
        resourceOffsetSizePairs.append({iconRes.dataOffsetShifted << resources.alignmentShiftCount, entry.size});
258
3.43M
        dataOffset += entry.size;
259
3.43M
    }
260
261
3.42M
    for (auto offsetSizePair : resourceOffsetSizePairs) {
262
3.42M
        if (!ds.device()->seek(offsetSizePair.first)) {
263
0
            return false;
264
0
        }
265
3.42M
        outputDevice->write(ds.device()->read(offsetSizePair.second));
266
3.42M
    }
267
268
1.21k
    return true;
269
1.21k
}
270
271
// Win32 Portable Executable
272
273
constexpr quint16 PeOptionalHeaderMagicPe32 = 0x010b;
274
constexpr quint16 PeOptionalHeaderMagicPe32Plus = 0x020b;
275
constexpr quint32 PeSubdirBitMask = 0x80000000;
276
277
constexpr int PeSignatureSize = 4;
278
constexpr int PeFileHeaderSize = 20;
279
constexpr int PeOffsetToDataDirectoryPe32 = 120;
280
constexpr int PeOffsetToDataDirectoryPe32Plus = 136;
281
constexpr int PeDataDirectorySize = 8;
282
283
enum class PeDataDirectoryIndex {
284
    Resource = 2,
285
};
286
287
struct PeFileHeader {
288
    quint16 machine;
289
    quint16 numSections;
290
    quint32 timestamp;
291
    quint32 offsetToSymbolTable;
292
    quint32 numberOfSymbols;
293
    quint16 sizeOfOptionalHeader;
294
    quint16 fileCharacteristics;
295
};
296
297
struct PeDataDirectory {
298
    quint32 virtualAddress, size;
299
};
300
301
struct PeSection {
302
    char name[8];
303
    quint32 virtualSize, virtualAddress;
304
    quint32 sizeOfRawData, pointerToRawData;
305
    quint32 pointerToRelocs, pointerToLineNums;
306
    quint16 numRelocs, numLineNums;
307
    quint32 characteristics;
308
};
309
310
struct PeResourceDirectoryTable {
311
    quint32 characteristics;
312
    quint32 timestamp;
313
    quint16 majorVersion, minorVersion;
314
    quint16 numNameEntries, numIDEntries;
315
};
316
317
struct PeResourceDirectoryEntry {
318
    quint32 resourceId, offset;
319
};
320
321
struct PeResourceDataEntry {
322
    quint32 dataAddress;
323
    quint32 size;
324
    quint32 codepage;
325
    quint32 reserved;
326
};
327
328
QDataStream &operator>>(QDataStream &s, PeFileHeader &v)
329
1.28k
{
330
1.28k
    s >> v.machine >> v.numSections >> v.timestamp >> v.offsetToSymbolTable >> v.numberOfSymbols >> v.sizeOfOptionalHeader >> v.fileCharacteristics;
331
1.28k
    return s;
332
1.28k
}
333
334
QDataStream &operator>>(QDataStream &s, PeDataDirectory &v)
335
1.23k
{
336
1.23k
    s >> v.virtualAddress >> v.size;
337
1.23k
    return s;
338
1.23k
}
339
340
QDataStream &operator>>(QDataStream &s, PeSection &v)
341
16.7M
{
342
16.7M
    s.readRawData(v.name, sizeof(v.name));
343
16.7M
    s >> v.virtualSize >> v.virtualAddress >> v.sizeOfRawData >> v.pointerToRawData >> v.pointerToRelocs >> v.pointerToLineNums >> v.numRelocs >> v.numLineNums
344
16.7M
        >> v.characteristics;
345
16.7M
    return s;
346
16.7M
}
347
348
QDataStream &operator>>(QDataStream &s, PeResourceDirectoryTable &v)
349
59.2k
{
350
59.2k
    s >> v.characteristics >> v.timestamp >> v.majorVersion >> v.minorVersion >> v.numNameEntries >> v.numIDEntries;
351
59.2k
    return s;
352
59.2k
}
353
354
QDataStream &operator>>(QDataStream &s, PeResourceDirectoryEntry &v)
355
31.6M
{
356
31.6M
    s >> v.resourceId >> v.offset;
357
31.6M
    return s;
358
31.6M
}
359
360
QDataStream &operator>>(QDataStream &s, PeResourceDataEntry &v)
361
12.9M
{
362
12.9M
    s >> v.dataAddress >> v.size >> v.codepage >> v.reserved;
363
12.9M
    return s;
364
12.9M
}
365
366
qint64 addressToOffset(const QVector<PeSection> &sections, quint32 rva)
367
84.3k
{
368
11.3M
    for (int i = 0; i < sections.size(); i++) {
369
11.3M
        auto sectionBegin = sections[i].virtualAddress;
370
11.3M
        auto effectiveSize = sections[i].sizeOfRawData;
371
11.3M
        if (sections[i].virtualSize) {
372
93.5k
            effectiveSize = std::min(effectiveSize, sections[i].virtualSize);
373
93.5k
        }
374
11.3M
        auto sectionEnd = sections[i].virtualAddress + effectiveSize;
375
11.3M
        if (rva >= sectionBegin && rva < sectionEnd) {
376
22.3k
            return rva - sectionBegin + sections[i].pointerToRawData;
377
22.3k
        }
378
11.3M
    }
379
62.0k
    return -1;
380
84.3k
}
381
382
QVector<PeResourceDirectoryEntry> readResourceDataDirectoryEntry(QDataStream &ds)
383
59.2k
{
384
59.2k
    PeResourceDirectoryTable table;
385
59.2k
    ds >> table;
386
59.2k
    QVector<PeResourceDirectoryEntry> entries;
387
31.7M
    for (int i = 0; i < table.numNameEntries + table.numIDEntries; i++) {
388
31.6M
        PeResourceDirectoryEntry entry;
389
31.6M
        ds >> entry;
390
31.6M
        entries.append(entry);
391
31.6M
    }
392
59.2k
    return entries;
393
59.2k
}
394
395
bool readPortableExecutablePrimaryIcon(QDataStream &ds, const DosHeader &dosHeader, QIODevice *outputDevice)
396
3.22k
{
397
3.22k
    PeFileHeader fileHeader;
398
3.22k
    bool isPe32Plus;
399
3.22k
    QMap<quint32, PeResourceDataEntry> iconResources;
400
3.22k
    std::optional<PeResourceDataEntry> primaryIconGroupResource;
401
3.22k
    QVector<PeSection> sections;
402
3.22k
    QDataStream out{outputDevice};
403
404
3.22k
    out.setByteOrder(QDataStream::LittleEndian);
405
406
    // Seek to + verify PE header. We're at the file header after this.
407
3.22k
    if (!ds.device()->seek(dosHeader.newHeaderOffset)) {
408
0
        return false;
409
0
    }
410
411
3.22k
    char signature[4];
412
3.22k
    if (ds.readRawData(signature, sizeof(signature)) == -1) {
413
0
        return false;
414
0
    }
415
416
3.22k
    if (signature[0] != 'P' || signature[1] != 'E' || signature[2] != 0 || signature[3] != 0) {
417
1.93k
        return false;
418
1.93k
    }
419
420
1.28k
    ds >> fileHeader;
421
422
    // Read optional header magic to determine if this is PE32 or PE32+.
423
1.28k
    quint16 optMagic;
424
1.28k
    ds >> optMagic;
425
426
1.28k
    switch (optMagic) {
427
477
    case PeOptionalHeaderMagicPe32:
428
477
        isPe32Plus = false;
429
477
        break;
430
431
756
    case PeOptionalHeaderMagicPe32Plus:
432
756
        isPe32Plus = true;
433
756
        break;
434
435
52
    default:
436
52
        return false;
437
1.28k
    }
438
439
    // Read section table now, so we can interpret RVAs.
440
1.23k
    quint64 sectionTableOffset = dosHeader.newHeaderOffset;
441
1.23k
    sectionTableOffset += PeSignatureSize + PeFileHeaderSize;
442
1.23k
    sectionTableOffset += fileHeader.sizeOfOptionalHeader;
443
1.23k
    if (!ds.device()->seek(sectionTableOffset)) {
444
0
        return false;
445
0
    }
446
447
16.7M
    for (int i = 0; i < fileHeader.numSections; i++) {
448
16.7M
        PeSection section;
449
16.7M
        ds >> section;
450
16.7M
        sections.append(section);
451
16.7M
    }
452
453
    // Find resource directory.
454
1.23k
    qint64 dataDirOffset = dosHeader.newHeaderOffset;
455
1.23k
    if (isPe32Plus) {
456
756
        dataDirOffset += PeOffsetToDataDirectoryPe32Plus;
457
756
    } else {
458
477
        dataDirOffset += PeOffsetToDataDirectoryPe32;
459
477
    }
460
1.23k
    dataDirOffset += qint64(PeDataDirectoryIndex::Resource) * PeDataDirectorySize;
461
1.23k
    if (!ds.device()->seek(dataDirOffset)) {
462
0
        return false;
463
0
    }
464
1.23k
    PeDataDirectory resourceDirectory;
465
1.23k
    ds >> resourceDirectory;
466
467
    // Read resource tree.
468
1.23k
    auto resourceOffset = addressToOffset(sections, resourceDirectory.virtualAddress);
469
1.23k
    if (resourceOffset < 0) {
470
347
        return false;
471
347
    }
472
473
886
    if (!ds.device()->seek(resourceOffset)) {
474
0
        return false;
475
0
    }
476
477
886
    const auto level1 = readResourceDataDirectoryEntry(ds);
478
479
8.83M
    for (auto entry1 : level1) {
480
8.83M
        if ((entry1.offset & PeSubdirBitMask) == 0)
481
8.82M
            continue;
482
10.0k
        if (!ds.device()->seek(resourceOffset + (entry1.offset & ~PeSubdirBitMask))) {
483
0
            return false;
484
0
        }
485
486
10.0k
        const auto level2 = readResourceDataDirectoryEntry(ds);
487
488
9.71M
        for (auto entry2 : level2) {
489
9.71M
            if ((entry2.offset & PeSubdirBitMask) == 0)
490
9.66M
                continue;
491
48.3k
            if (!ds.device()->seek(resourceOffset + (entry2.offset & ~PeSubdirBitMask))) {
492
0
                return false;
493
0
            }
494
495
            // Read subdirectory.
496
48.3k
            const auto level3 = readResourceDataDirectoryEntry(ds);
497
498
13.0M
            for (auto entry3 : level3) {
499
13.0M
                if ((entry3.offset & PeSubdirBitMask) == PeSubdirBitMask)
500
163k
                    continue;
501
12.9M
                if (!ds.device()->seek(resourceOffset + (entry3.offset & ~PeSubdirBitMask))) {
502
0
                    return false;
503
0
                }
504
505
                // Read data.
506
12.9M
                PeResourceDataEntry dataEntry;
507
12.9M
                ds >> dataEntry;
508
509
12.9M
                switch (ResourceType(entry1.resourceId)) {
510
5.22M
                case ResourceType::Icon:
511
5.22M
                    iconResources[entry2.resourceId] = dataEntry;
512
5.22M
                    break;
513
514
1.82M
                case ResourceType::GroupIcon:
515
1.82M
                    if (!primaryIconGroupResource.has_value()) {
516
339
                        primaryIconGroupResource = dataEntry;
517
339
                    }
518
12.9M
                }
519
12.9M
            }
520
48.3k
        }
521
10.0k
    }
522
523
886
    if (!primaryIconGroupResource.has_value()) {
524
547
        return false;
525
547
    }
526
527
339
    if (!ds.device()->seek(addressToOffset(sections, primaryIconGroupResource->dataAddress))) {
528
85
        return false;
529
85
    }
530
531
254
    RtGroupIconDirectory primaryIconGroup;
532
254
    ds >> primaryIconGroup;
533
534
254
    IconDir icoFileHeader;
535
254
    icoFileHeader.reserved = 0;
536
254
    icoFileHeader.type = 1; // Always 1 for ico files.
537
254
    icoFileHeader.count = primaryIconGroup.count;
538
254
    out << icoFileHeader;
539
540
254
    quint32 dataOffset = IconDirSize + IconDirEntrySize * primaryIconGroup.count;
541
254
    QVector<QPair<qint64, quint32>> resourceOffsetSizePairs;
542
543
83.0k
    for (int i = 0; i < primaryIconGroup.count; i++) {
544
82.8k
        RtGroupIconDirectoryEntry entry;
545
82.8k
        ds >> entry;
546
547
82.8k
        IconDirEntry icoFileEntry{entry, dataOffset};
548
82.8k
        out << icoFileEntry;
549
550
82.8k
        if (auto it = iconResources.find(entry.resourceId); it != iconResources.end()) {
551
82.7k
            PeResourceDataEntry iconResource = *it;
552
82.7k
            resourceOffsetSizePairs.append({addressToOffset(sections, iconResource.dataAddress), iconResource.size});
553
82.7k
            dataOffset += iconResource.size;
554
82.7k
        } else {
555
105
            return false;
556
105
        }
557
82.8k
    }
558
559
21.1k
    for (auto offsetSizePair : resourceOffsetSizePairs) {
560
21.1k
        if (!ds.device()->seek(offsetSizePair.first)) {
561
18
            return false;
562
18
        }
563
21.0k
        outputDevice->write(ds.device()->read(offsetSizePair.second));
564
21.0k
    }
565
566
131
    return true;
567
149
}
568
569
}
570
571
bool ExeUtils::loadIcoDataFromExe(QIODevice *inputDevice, QIODevice *outputDevice)
572
3.69k
{
573
3.69k
    QDataStream ds{inputDevice};
574
3.69k
    ds.setByteOrder(QDataStream::LittleEndian);
575
576
    // Read DOS header.
577
3.69k
    DosHeader dosHeader;
578
3.69k
    ds >> dosHeader;
579
580
    // Verify the MZ header.
581
3.69k
    if (dosHeader.signature[0] != 'M' || dosHeader.signature[1] != 'Z') {
582
473
        return false;
583
473
    }
584
585
3.22k
    if (readPortableExecutablePrimaryIcon(ds, dosHeader, outputDevice)) {
586
131
        return true;
587
131
    }
588
589
3.09k
    if (readNewExecutablePrimaryIcon(ds, dosHeader, outputDevice)) {
590
1.21k
        return true;
591
1.21k
    }
592
593
1.87k
    return false;
594
3.09k
}