Coverage Report

Created: 2026-07-15 07:31

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.96k
{
29
3.96k
    s.readRawData(v.signature, sizeof(v.signature));
30
3.96k
    s.device()->skip(58);
31
3.96k
    s >> v.newHeaderOffset;
32
3.96k
    return s;
33
3.96k
}
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
2.07k
{
60
2.07k
    s >> v.reserved >> v.type >> v.count;
61
2.07k
    return s;
62
2.07k
}
63
64
QDataStream &operator>>(QDataStream &s, RtGroupIconDirectoryEntry &v)
65
3.57M
{
66
3.57M
    s >> v.width >> v.height >> v.colorCount >> v.reserved >> v.numPlanes >> v.bpp >> v.size >> v.resourceId;
67
3.57M
    return s;
68
3.57M
}
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.57M
    {
91
3.57M
        width = entry.width;
92
3.57M
        height = entry.height;
93
3.57M
        colorCount = entry.colorCount;
94
3.57M
        reserved = entry.reserved;
95
3.57M
        numPlanes = entry.numPlanes;
96
3.57M
        bpp = entry.bpp;
97
3.57M
        size = entry.size;
98
3.57M
        imageOffset = dataOffset;
99
3.57M
    }
100
};
101
102
constexpr int IconDirEntrySize = 16;
103
104
QDataStream &operator<<(QDataStream &s, const IconDir &v)
105
2.07k
{
106
2.07k
    s << v.reserved << v.type << v.count;
107
2.07k
    return s;
108
2.07k
}
109
110
QDataStream &operator<<(QDataStream &s, const IconDirEntry &v)
111
3.57M
{
112
3.57M
    s << v.width << v.height << v.colorCount << v.reserved << v.numPlanes << v.bpp << v.size << v.imageOffset;
113
3.57M
    return s;
114
3.57M
}
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.92k
{
145
1.92k
    s.device()->skip(34);
146
1.92k
    s >> v.offsetOfResourceTable;
147
1.92k
    s.device()->skip(14);
148
1.92k
    s >> v.numberOfResourceSegments;
149
1.92k
    return s;
150
1.92k
}
151
152
QDataStream &operator>>(QDataStream &s, NeResource &v)
153
10.8M
{
154
10.8M
    s >> v.dataOffsetShifted >> v.dataLength >> v.flags >> v.resourceId >> v.resource[0] >> v.resource[1];
155
10.8M
    v.resourceId ^= 0x8000;
156
10.8M
    return s;
157
10.8M
}
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.92k
        return s;
164
1.92k
    }
165
8.67k
    s >> v.numResources >> v.resource[0] >> v.resource[1];
166
10.8M
    for (int i = 0; i < v.numResources; i++) {
167
10.8M
        NeResource resource;
168
10.8M
        s >> resource;
169
10.8M
        v.resources.append(resource);
170
10.8M
    }
171
8.67k
    return s;
172
10.5k
}
173
174
QDataStream &operator>>(QDataStream &s, NeResourceTable &v)
175
1.92k
{
176
1.92k
    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.92k
            break;
182
1.92k
        }
183
8.67k
        v.types[ResourceType(type.typeId ^ 0x8000)] = type;
184
8.67k
    }
185
1.92k
    return s;
186
1.92k
}
187
188
bool readNewExecutablePrimaryIcon(QDataStream &ds, const DosHeader &dosHeader, QIODevice *outputDevice)
189
3.19k
{
190
3.19k
    NeFileHeader fileHeader;
191
3.19k
    NeResourceTable resources;
192
3.19k
    QDataStream out{outputDevice};
193
194
3.19k
    out.setByteOrder(QDataStream::LittleEndian);
195
196
3.19k
    if (!ds.device()->seek(dosHeader.newHeaderOffset)) {
197
0
        return false;
198
0
    }
199
200
3.19k
    char signature[2];
201
3.19k
    ds.readRawData(signature, sizeof(signature));
202
203
3.19k
    if (signature[0] != 'N' || signature[1] != 'E') {
204
1.27k
        return false;
205
1.27k
    }
206
207
1.92k
    ds >> fileHeader;
208
1.92k
    if (!ds.device()->seek(dosHeader.newHeaderOffset + fileHeader.offsetOfResourceTable)) {
209
0
        return false;
210
0
    }
211
212
1.92k
    ds >> resources;
213
214
1.92k
    if (!resources.types.contains(ResourceType::GroupIcon) || !resources.types.contains(ResourceType::Icon)) {
215
282
        return false;
216
282
    }
217
218
1.64k
    auto iconResources = resources.types[ResourceType::Icon].resources;
219
220
1.64k
    auto iconGroupResources = resources.types[ResourceType::GroupIcon];
221
1.64k
    if (iconGroupResources.resources.empty()) {
222
14
        return false;
223
14
    }
224
225
1.62k
    auto iconGroupResource = iconGroupResources.resources.first();
226
1.62k
    if (!ds.device()->seek(iconGroupResource.dataOffsetShifted << resources.alignmentShiftCount)) {
227
12
        return false;
228
12
    }
229
230
1.61k
    RtGroupIconDirectory iconGroup;
231
1.61k
    ds >> iconGroup;
232
233
1.61k
    IconDir icoHeader;
234
1.61k
    icoHeader.reserved = 0;
235
1.61k
    icoHeader.type = 1; // Always 1 for ico files.
236
1.61k
    icoHeader.count = iconGroup.count;
237
1.61k
    out << icoHeader;
238
239
1.61k
    quint32 dataOffset = IconDirSize + IconDirEntrySize * iconGroup.count;
240
1.61k
    QVector<QPair<qint64, quint32>> resourceOffsetSizePairs;
241
242
794k
    for (int i = 0; i < iconGroup.count; i++) {
243
793k
        RtGroupIconDirectoryEntry entry;
244
793k
        ds >> entry;
245
246
817k
        auto it = std::find_if(iconResources.begin(), iconResources.end(), [&entry](const NeResource &res) {
247
817k
            return res.resourceId == entry.resourceId;
248
817k
        });
249
793k
        if (it == iconResources.end()) {
250
121
            return false;
251
121
        }
252
253
793k
        IconDirEntry icoEntry{entry, dataOffset};
254
793k
        out << icoEntry;
255
256
793k
        NeResource iconRes = *it;
257
793k
        if (entry.size != iconRes.dataLength) {
258
203
            return false;
259
203
        }
260
793k
        resourceOffsetSizePairs.append({iconRes.dataOffsetShifted << resources.alignmentShiftCount, entry.size});
261
793k
        dataOffset += entry.size;
262
793k
    }
263
264
792k
    for (auto offsetSizePair : resourceOffsetSizePairs) {
265
792k
        if (!ds.device()->seek(offsetSizePair.first)) {
266
0
            return false;
267
0
        }
268
792k
        outputDevice->write(ds.device()->read(offsetSizePair.second));
269
792k
    }
270
271
1.29k
    return true;
272
1.29k
}
273
274
// Win32 Portable Executable
275
276
constexpr quint16 PeOptionalHeaderMagicPe32 = 0x010b;
277
constexpr quint16 PeOptionalHeaderMagicPe32Plus = 0x020b;
278
constexpr quint32 PeSubdirBitMask = 0x80000000;
279
280
constexpr int PeSignatureSize = 4;
281
constexpr int PeFileHeaderSize = 20;
282
constexpr int PeOffsetToDataDirectoryPe32 = 120;
283
constexpr int PeOffsetToDataDirectoryPe32Plus = 136;
284
constexpr int PeDataDirectorySize = 8;
285
286
enum class PeDataDirectoryIndex {
287
    Resource = 2,
288
};
289
290
struct PeFileHeader {
291
    quint16 machine;
292
    quint16 numSections;
293
    quint32 timestamp;
294
    quint32 offsetToSymbolTable;
295
    quint32 numberOfSymbols;
296
    quint16 sizeOfOptionalHeader;
297
    quint16 fileCharacteristics;
298
};
299
300
struct PeDataDirectory {
301
    quint32 virtualAddress, size;
302
};
303
304
struct PeSection {
305
    char name[8];
306
    quint32 virtualSize, virtualAddress;
307
    quint32 sizeOfRawData, pointerToRawData;
308
    quint32 pointerToRelocs, pointerToLineNums;
309
    quint16 numRelocs, numLineNums;
310
    quint32 characteristics;
311
};
312
313
struct PeResourceDirectoryTable {
314
    quint32 characteristics;
315
    quint32 timestamp;
316
    quint16 majorVersion, minorVersion;
317
    quint16 numNameEntries, numIDEntries;
318
};
319
320
struct PeResourceDirectoryEntry {
321
    quint32 resourceId, offset;
322
};
323
324
struct PeResourceDataEntry {
325
    quint32 dataAddress;
326
    quint32 size;
327
    quint32 codepage;
328
    quint32 reserved;
329
};
330
331
QDataStream &operator>>(QDataStream &s, PeFileHeader &v)
332
1.33k
{
333
1.33k
    s >> v.machine >> v.numSections >> v.timestamp >> v.offsetToSymbolTable >> v.numberOfSymbols >> v.sizeOfOptionalHeader >> v.fileCharacteristics;
334
1.33k
    return s;
335
1.33k
}
336
337
QDataStream &operator>>(QDataStream &s, PeDataDirectory &v)
338
1.27k
{
339
1.27k
    s >> v.virtualAddress >> v.size;
340
1.27k
    return s;
341
1.27k
}
342
343
QDataStream &operator>>(QDataStream &s, PeSection &v)
344
14.5M
{
345
14.5M
    s.readRawData(v.name, sizeof(v.name));
346
14.5M
    s >> v.virtualSize >> v.virtualAddress >> v.sizeOfRawData >> v.pointerToRawData >> v.pointerToRelocs >> v.pointerToLineNums >> v.numRelocs >> v.numLineNums
347
14.5M
        >> v.characteristics;
348
14.5M
    return s;
349
14.5M
}
350
351
QDataStream &operator>>(QDataStream &s, PeResourceDirectoryTable &v)
352
50.7k
{
353
50.7k
    s >> v.characteristics >> v.timestamp >> v.majorVersion >> v.minorVersion >> v.numNameEntries >> v.numIDEntries;
354
50.7k
    return s;
355
50.7k
}
356
357
QDataStream &operator>>(QDataStream &s, PeResourceDirectoryEntry &v)
358
35.6M
{
359
35.6M
    s >> v.resourceId >> v.offset;
360
35.6M
    return s;
361
35.6M
}
362
363
QDataStream &operator>>(QDataStream &s, PeResourceDataEntry &v)
364
15.9M
{
365
15.9M
    s >> v.dataAddress >> v.size >> v.codepage >> v.reserved;
366
15.9M
    return s;
367
15.9M
}
368
369
qint64 addressToOffset(const QVector<PeSection> &sections, quint32 rva)
370
2.78M
{
371
32.4M
    for (int i = 0; i < sections.size(); i++) {
372
32.1M
        auto sectionBegin = sections[i].virtualAddress;
373
32.1M
        auto effectiveSize = sections[i].sizeOfRawData;
374
32.1M
        if (sections[i].virtualSize) {
375
3.17M
            effectiveSize = std::min(effectiveSize, sections[i].virtualSize);
376
3.17M
        }
377
32.1M
        auto sectionEnd = sections[i].virtualAddress + effectiveSize;
378
32.1M
        if (rva >= sectionBegin && rva < sectionEnd) {
379
2.50M
            return rva - sectionBegin + sections[i].pointerToRawData;
380
2.50M
        }
381
32.1M
    }
382
281k
    return -1;
383
2.78M
}
384
385
QVector<PeResourceDirectoryEntry> readResourceDataDirectoryEntry(QDataStream &ds)
386
50.7k
{
387
50.7k
    PeResourceDirectoryTable table;
388
50.7k
    ds >> table;
389
50.7k
    QVector<PeResourceDirectoryEntry> entries;
390
35.6M
    for (int i = 0; i < table.numNameEntries + table.numIDEntries; i++) {
391
35.6M
        PeResourceDirectoryEntry entry;
392
35.6M
        ds >> entry;
393
35.6M
        entries.append(entry);
394
35.6M
    }
395
50.7k
    return entries;
396
50.7k
}
397
398
bool readPortableExecutablePrimaryIcon(QDataStream &ds, const DosHeader &dosHeader, QIODevice *outputDevice)
399
3.51k
{
400
3.51k
    PeFileHeader fileHeader;
401
3.51k
    bool isPe32Plus;
402
3.51k
    QMap<quint32, PeResourceDataEntry> iconResources;
403
3.51k
    std::optional<PeResourceDataEntry> primaryIconGroupResource;
404
3.51k
    QVector<PeSection> sections;
405
3.51k
    QDataStream out{outputDevice};
406
407
3.51k
    out.setByteOrder(QDataStream::LittleEndian);
408
409
    // Seek to + verify PE header. We're at the file header after this.
410
3.51k
    if (!ds.device()->seek(dosHeader.newHeaderOffset)) {
411
0
        return false;
412
0
    }
413
414
3.51k
    char signature[4];
415
3.51k
    if (ds.readRawData(signature, sizeof(signature)) == -1) {
416
0
        return false;
417
0
    }
418
419
3.51k
    if (signature[0] != 'P' || signature[1] != 'E' || signature[2] != 0 || signature[3] != 0) {
420
2.17k
        return false;
421
2.17k
    }
422
423
1.33k
    ds >> fileHeader;
424
425
    // Read optional header magic to determine if this is PE32 or PE32+.
426
1.33k
    quint16 optMagic;
427
1.33k
    ds >> optMagic;
428
429
1.33k
    switch (optMagic) {
430
372
    case PeOptionalHeaderMagicPe32:
431
372
        isPe32Plus = false;
432
372
        break;
433
434
902
    case PeOptionalHeaderMagicPe32Plus:
435
902
        isPe32Plus = true;
436
902
        break;
437
438
64
    default:
439
64
        return false;
440
1.33k
    }
441
442
    // Read section table now, so we can interpret RVAs.
443
1.27k
    quint64 sectionTableOffset = dosHeader.newHeaderOffset;
444
1.27k
    sectionTableOffset += PeSignatureSize + PeFileHeaderSize;
445
1.27k
    sectionTableOffset += fileHeader.sizeOfOptionalHeader;
446
1.27k
    if (!ds.device()->seek(sectionTableOffset)) {
447
0
        return false;
448
0
    }
449
450
14.5M
    for (int i = 0; i < fileHeader.numSections; i++) {
451
14.5M
        PeSection section;
452
14.5M
        ds >> section;
453
14.5M
        sections.append(section);
454
14.5M
    }
455
456
    // Find resource directory.
457
1.27k
    qint64 dataDirOffset = dosHeader.newHeaderOffset;
458
1.27k
    if (isPe32Plus) {
459
902
        dataDirOffset += PeOffsetToDataDirectoryPe32Plus;
460
902
    } else {
461
372
        dataDirOffset += PeOffsetToDataDirectoryPe32;
462
372
    }
463
1.27k
    dataDirOffset += qint64(PeDataDirectoryIndex::Resource) * PeDataDirectorySize;
464
1.27k
    if (!ds.device()->seek(dataDirOffset)) {
465
0
        return false;
466
0
    }
467
1.27k
    PeDataDirectory resourceDirectory;
468
1.27k
    ds >> resourceDirectory;
469
470
    // Read resource tree.
471
1.27k
    auto resourceOffset = addressToOffset(sections, resourceDirectory.virtualAddress);
472
1.27k
    if (resourceOffset < 0) {
473
286
        return false;
474
286
    }
475
476
988
    if (!ds.device()->seek(resourceOffset)) {
477
0
        return false;
478
0
    }
479
480
988
    const auto level1 = readResourceDataDirectoryEntry(ds);
481
482
7.84M
    for (auto entry1 : level1) {
483
7.84M
        if ((entry1.offset & PeSubdirBitMask) == 0)
484
7.83M
            continue;
485
9.23k
        if (!ds.device()->seek(resourceOffset + (entry1.offset & ~PeSubdirBitMask))) {
486
0
            return false;
487
0
        }
488
489
9.23k
        const auto level2 = readResourceDataDirectoryEntry(ds);
490
491
11.6M
        for (auto entry2 : level2) {
492
11.6M
            if ((entry2.offset & PeSubdirBitMask) == 0)
493
11.6M
                continue;
494
40.5k
            if (!ds.device()->seek(resourceOffset + (entry2.offset & ~PeSubdirBitMask))) {
495
0
                return false;
496
0
            }
497
498
            // Read subdirectory.
499
40.5k
            const auto level3 = readResourceDataDirectoryEntry(ds);
500
501
16.1M
            for (auto entry3 : level3) {
502
16.1M
                if ((entry3.offset & PeSubdirBitMask) == PeSubdirBitMask)
503
201k
                    continue;
504
15.9M
                if (!ds.device()->seek(resourceOffset + (entry3.offset & ~PeSubdirBitMask))) {
505
0
                    return false;
506
0
                }
507
508
                // Read data.
509
15.9M
                PeResourceDataEntry dataEntry;
510
15.9M
                ds >> dataEntry;
511
512
15.9M
                switch (ResourceType(entry1.resourceId)) {
513
4.99M
                case ResourceType::Icon:
514
4.99M
                    iconResources[entry2.resourceId] = dataEntry;
515
4.99M
                    break;
516
517
4.00M
                case ResourceType::GroupIcon:
518
4.00M
                    if (!primaryIconGroupResource.has_value()) {
519
573
                        primaryIconGroupResource = dataEntry;
520
573
                    }
521
15.9M
                }
522
15.9M
            }
523
40.5k
        }
524
9.23k
    }
525
526
988
    if (!primaryIconGroupResource.has_value()) {
527
415
        return false;
528
415
    }
529
530
573
    if (!ds.device()->seek(addressToOffset(sections, primaryIconGroupResource->dataAddress))) {
531
117
        return false;
532
117
    }
533
534
456
    RtGroupIconDirectory primaryIconGroup;
535
456
    ds >> primaryIconGroup;
536
537
456
    IconDir icoFileHeader;
538
456
    icoFileHeader.reserved = 0;
539
456
    icoFileHeader.type = 1; // Always 1 for ico files.
540
456
    icoFileHeader.count = primaryIconGroup.count;
541
456
    out << icoFileHeader;
542
543
456
    quint32 dataOffset = IconDirSize + IconDirEntrySize * primaryIconGroup.count;
544
456
    QVector<QPair<qint64, quint32>> resourceOffsetSizePairs;
545
546
2.78M
    for (int i = 0; i < primaryIconGroup.count; i++) {
547
2.78M
        RtGroupIconDirectoryEntry entry;
548
2.78M
        ds >> entry;
549
550
2.78M
        IconDirEntry icoFileEntry{entry, dataOffset};
551
2.78M
        out << icoFileEntry;
552
553
2.78M
        if (auto it = iconResources.find(entry.resourceId); it != iconResources.end()) {
554
2.78M
            PeResourceDataEntry iconResource = *it;
555
2.78M
            resourceOffsetSizePairs.append({addressToOffset(sections, iconResource.dataAddress), iconResource.size});
556
2.78M
            dataOffset += iconResource.size;
557
2.78M
        } else {
558
103
            return false;
559
103
        }
560
2.78M
    }
561
562
2.44M
    for (auto offsetSizePair : resourceOffsetSizePairs) {
563
2.44M
        if (!ds.device()->seek(offsetSizePair.first)) {
564
34
            return false;
565
34
        }
566
2.44M
        outputDevice->write(ds.device()->read(offsetSizePair.second));
567
2.44M
    }
568
569
319
    return true;
570
353
}
571
572
}
573
574
bool ExeUtils::loadIcoDataFromExe(QIODevice *inputDevice, QIODevice *outputDevice)
575
3.96k
{
576
3.96k
    QDataStream ds{inputDevice};
577
3.96k
    ds.setByteOrder(QDataStream::LittleEndian);
578
579
    // Read DOS header.
580
3.96k
    DosHeader dosHeader;
581
3.96k
    ds >> dosHeader;
582
583
    // Verify the MZ header.
584
3.96k
    if (dosHeader.signature[0] != 'M' || dosHeader.signature[1] != 'Z') {
585
450
        return false;
586
450
    }
587
588
3.51k
    if (readPortableExecutablePrimaryIcon(ds, dosHeader, outputDevice)) {
589
319
        return true;
590
319
    }
591
592
3.19k
    if (readNewExecutablePrimaryIcon(ds, dosHeader, outputDevice)) {
593
1.29k
        return true;
594
1.29k
    }
595
596
1.90k
    return false;
597
3.19k
}