Coverage Report

Created: 2026-10-28 07:09

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
4.56k
{
29
4.56k
    s.readRawData(v.signature, sizeof(v.signature));
30
4.56k
    s.device()->skip(58);
31
4.56k
    s >> v.newHeaderOffset;
32
4.56k
    return s;
33
4.56k
}
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.36k
{
60
2.36k
    s >> v.reserved >> v.type >> v.count;
61
2.36k
    return s;
62
2.36k
}
63
64
QDataStream &operator>>(QDataStream &s, RtGroupIconDirectoryEntry &v)
65
7.32M
{
66
7.32M
    s >> v.width >> v.height >> v.colorCount >> v.reserved >> v.numPlanes >> v.bpp >> v.size >> v.resourceId;
67
7.32M
    return s;
68
7.32M
}
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
7.32M
    {
91
7.32M
        width = entry.width;
92
7.32M
        height = entry.height;
93
7.32M
        colorCount = entry.colorCount;
94
7.32M
        reserved = entry.reserved;
95
7.32M
        numPlanes = entry.numPlanes;
96
7.32M
        bpp = entry.bpp;
97
7.32M
        size = entry.size;
98
7.32M
        imageOffset = dataOffset;
99
7.32M
    }
100
};
101
102
constexpr int IconDirEntrySize = 16;
103
104
QDataStream &operator<<(QDataStream &s, const IconDir &v)
105
2.36k
{
106
2.36k
    s << v.reserved << v.type << v.count;
107
2.36k
    return s;
108
2.36k
}
109
110
QDataStream &operator<<(QDataStream &s, const IconDirEntry &v)
111
7.32M
{
112
7.32M
    s << v.width << v.height << v.colorCount << v.reserved << v.numPlanes << v.bpp << v.size << v.imageOffset;
113
7.32M
    return s;
114
7.32M
}
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.68k
{
145
1.68k
    s.device()->skip(34);
146
1.68k
    s >> v.offsetOfResourceTable;
147
1.68k
    s.device()->skip(14);
148
1.68k
    s >> v.numberOfResourceSegments;
149
1.68k
    return s;
150
1.68k
}
151
152
QDataStream &operator>>(QDataStream &s, NeResource &v)
153
13.5M
{
154
13.5M
    s >> v.dataOffsetShifted >> v.dataLength >> v.flags >> v.resourceId >> v.resource[0] >> v.resource[1];
155
13.5M
    v.resourceId ^= 0x8000;
156
13.5M
    return s;
157
13.5M
}
158
159
QDataStream &operator>>(QDataStream &s, NeResourceTable::Type &v)
160
9.16k
{
161
9.16k
    s >> v.typeId;
162
9.16k
    if (v.typeId == 0) {
163
1.68k
        return s;
164
1.68k
    }
165
7.47k
    s >> v.numResources >> v.resource[0] >> v.resource[1];
166
13.5M
    for (int i = 0; i < v.numResources; i++) {
167
13.5M
        NeResource resource;
168
13.5M
        s >> resource;
169
13.5M
        v.resources.append(resource);
170
13.5M
    }
171
7.47k
    return s;
172
9.16k
}
173
174
QDataStream &operator>>(QDataStream &s, NeResourceTable &v)
175
1.68k
{
176
1.68k
    s >> v.alignmentShiftCount;
177
9.16k
    while (1) {
178
9.16k
        NeResourceTable::Type type;
179
9.16k
        s >> type;
180
9.16k
        if (!type.typeId) {
181
1.68k
            break;
182
1.68k
        }
183
7.47k
        v.types[ResourceType(type.typeId ^ 0x8000)] = type;
184
7.47k
    }
185
1.68k
    return s;
186
1.68k
}
187
188
bool readNewExecutablePrimaryIcon(QDataStream &ds, const DosHeader &dosHeader, QIODevice *outputDevice)
189
3.21k
{
190
3.21k
    NeFileHeader fileHeader;
191
3.21k
    NeResourceTable resources;
192
3.21k
    QDataStream out{outputDevice};
193
194
3.21k
    out.setByteOrder(QDataStream::LittleEndian);
195
196
3.21k
    if (!ds.device()->seek(dosHeader.newHeaderOffset)) {
197
0
        return false;
198
0
    }
199
200
3.21k
    char signature[2];
201
3.21k
    ds.readRawData(signature, sizeof(signature));
202
203
3.21k
    if (signature[0] != 'N' || signature[1] != 'E') {
204
1.53k
        return false;
205
1.53k
    }
206
207
1.68k
    ds >> fileHeader;
208
1.68k
    if (!ds.device()->seek(dosHeader.newHeaderOffset + fileHeader.offsetOfResourceTable)) {
209
0
        return false;
210
0
    }
211
212
1.68k
    ds >> resources;
213
214
1.68k
    if (!resources.types.contains(ResourceType::GroupIcon) || !resources.types.contains(ResourceType::Icon)) {
215
391
        return false;
216
391
    }
217
218
1.29k
    auto iconResources = resources.types[ResourceType::Icon].resources;
219
220
1.29k
    auto iconGroupResources = resources.types[ResourceType::GroupIcon];
221
1.29k
    if (iconGroupResources.resources.empty()) {
222
12
        return false;
223
12
    }
224
225
1.28k
    auto iconGroupResource = iconGroupResources.resources.first();
226
1.28k
    if (!ds.device()->seek(iconGroupResource.dataOffsetShifted << resources.alignmentShiftCount)) {
227
13
        return false;
228
13
    }
229
230
1.27k
    RtGroupIconDirectory iconGroup;
231
1.27k
    ds >> iconGroup;
232
233
1.27k
    IconDir icoHeader;
234
1.27k
    icoHeader.reserved = 0;
235
1.27k
    icoHeader.type = 1; // Always 1 for ico files.
236
1.27k
    icoHeader.count = iconGroup.count;
237
1.27k
    out << icoHeader;
238
239
1.27k
    quint32 dataOffset = IconDirSize + IconDirEntrySize * iconGroup.count;
240
1.27k
    QVector<QPair<qint64, quint32>> resourceOffsetSizePairs;
241
242
970k
    for (int i = 0; i < iconGroup.count; i++) {
243
969k
        RtGroupIconDirectoryEntry entry;
244
969k
        ds >> entry;
245
246
1.36M
        auto it = std::find_if(iconResources.begin(), iconResources.end(), [&entry](const NeResource &res) {
247
1.36M
            return res.resourceId == entry.resourceId;
248
1.36M
        });
249
969k
        if (it == iconResources.end()) {
250
150
            return false;
251
150
        }
252
253
969k
        IconDirEntry icoEntry{entry, dataOffset};
254
969k
        out << icoEntry;
255
256
969k
        NeResource iconRes = *it;
257
969k
        if (entry.size != iconRes.dataLength) {
258
168
            return false;
259
168
        }
260
969k
        resourceOffsetSizePairs.append({iconRes.dataOffsetShifted << resources.alignmentShiftCount, entry.size});
261
969k
        dataOffset += entry.size;
262
969k
    }
263
264
969k
    for (auto offsetSizePair : resourceOffsetSizePairs) {
265
969k
        if (!ds.device()->seek(offsetSizePair.first)) {
266
0
            return false;
267
0
        }
268
969k
        outputDevice->write(ds.device()->read(offsetSizePair.second));
269
969k
    }
270
271
955
    return true;
272
955
}
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
2.07k
{
333
2.07k
    s >> v.machine >> v.numSections >> v.timestamp >> v.offsetToSymbolTable >> v.numberOfSymbols >> v.sizeOfOptionalHeader >> v.fileCharacteristics;
334
2.07k
    return s;
335
2.07k
}
336
337
QDataStream &operator>>(QDataStream &s, PeDataDirectory &v)
338
1.97k
{
339
1.97k
    s >> v.virtualAddress >> v.size;
340
1.97k
    return s;
341
1.97k
}
342
343
QDataStream &operator>>(QDataStream &s, PeSection &v)
344
13.1M
{
345
13.1M
    s.readRawData(v.name, sizeof(v.name));
346
13.1M
    s >> v.virtualSize >> v.virtualAddress >> v.sizeOfRawData >> v.pointerToRawData >> v.pointerToRelocs >> v.pointerToLineNums >> v.numRelocs >> v.numLineNums
347
13.1M
        >> v.characteristics;
348
13.1M
    return s;
349
13.1M
}
350
351
QDataStream &operator>>(QDataStream &s, PeResourceDirectoryTable &v)
352
59.5k
{
353
59.5k
    s >> v.characteristics >> v.timestamp >> v.majorVersion >> v.minorVersion >> v.numNameEntries >> v.numIDEntries;
354
59.5k
    return s;
355
59.5k
}
356
357
QDataStream &operator>>(QDataStream &s, PeResourceDirectoryEntry &v)
358
41.2M
{
359
41.2M
    s >> v.resourceId >> v.offset;
360
41.2M
    return s;
361
41.2M
}
362
363
QDataStream &operator>>(QDataStream &s, PeResourceDataEntry &v)
364
18.0M
{
365
18.0M
    s >> v.dataAddress >> v.size >> v.codepage >> v.reserved;
366
18.0M
    return s;
367
18.0M
}
368
369
qint64 addressToOffset(const QVector<PeSection> &sections, quint32 rva)
370
6.36M
{
371
38.7M
    for (int i = 0; i < sections.size(); i++) {
372
38.4M
        auto sectionBegin = sections[i].virtualAddress;
373
38.4M
        auto effectiveSize = sections[i].sizeOfRawData;
374
38.4M
        if (sections[i].virtualSize) {
375
6.96M
            effectiveSize = std::min(effectiveSize, sections[i].virtualSize);
376
6.96M
        }
377
38.4M
        auto sectionEnd = sections[i].virtualAddress + effectiveSize;
378
38.4M
        if (rva >= sectionBegin && rva < sectionEnd) {
379
6.01M
            return rva - sectionBegin + sections[i].pointerToRawData;
380
6.01M
        }
381
38.4M
    }
382
348k
    return -1;
383
6.36M
}
384
385
QVector<PeResourceDirectoryEntry> readResourceDataDirectoryEntry(QDataStream &ds)
386
59.5k
{
387
59.5k
    PeResourceDirectoryTable table;
388
59.5k
    ds >> table;
389
59.5k
    QVector<PeResourceDirectoryEntry> entries;
390
41.3M
    for (int i = 0; i < table.numNameEntries + table.numIDEntries; i++) {
391
41.2M
        PeResourceDirectoryEntry entry;
392
41.2M
        ds >> entry;
393
41.2M
        entries.append(entry);
394
41.2M
    }
395
59.5k
    return entries;
396
59.5k
}
397
398
bool readPortableExecutablePrimaryIcon(QDataStream &ds, const DosHeader &dosHeader, QIODevice *outputDevice)
399
4.08k
{
400
4.08k
    PeFileHeader fileHeader;
401
4.08k
    bool isPe32Plus;
402
4.08k
    QMap<quint32, PeResourceDataEntry> iconResources;
403
4.08k
    std::optional<PeResourceDataEntry> primaryIconGroupResource;
404
4.08k
    QVector<PeSection> sections;
405
4.08k
    QDataStream out{outputDevice};
406
407
4.08k
    out.setByteOrder(QDataStream::LittleEndian);
408
409
    // Seek to + verify PE header. We're at the file header after this.
410
4.08k
    if (!ds.device()->seek(dosHeader.newHeaderOffset)) {
411
0
        return false;
412
0
    }
413
414
4.08k
    char signature[4];
415
4.08k
    if (ds.readRawData(signature, sizeof(signature)) == -1) {
416
0
        return false;
417
0
    }
418
419
4.08k
    if (signature[0] != 'P' || signature[1] != 'E' || signature[2] != 0 || signature[3] != 0) {
420
2.00k
        return false;
421
2.00k
    }
422
423
2.07k
    ds >> fileHeader;
424
425
    // Read optional header magic to determine if this is PE32 or PE32+.
426
2.07k
    quint16 optMagic;
427
2.07k
    ds >> optMagic;
428
429
2.07k
    switch (optMagic) {
430
436
    case PeOptionalHeaderMagicPe32:
431
436
        isPe32Plus = false;
432
436
        break;
433
434
1.54k
    case PeOptionalHeaderMagicPe32Plus:
435
1.54k
        isPe32Plus = true;
436
1.54k
        break;
437
438
100
    default:
439
100
        return false;
440
2.07k
    }
441
442
    // Read section table now, so we can interpret RVAs.
443
1.97k
    quint64 sectionTableOffset = dosHeader.newHeaderOffset;
444
1.97k
    sectionTableOffset += PeSignatureSize + PeFileHeaderSize;
445
1.97k
    sectionTableOffset += fileHeader.sizeOfOptionalHeader;
446
1.97k
    if (!ds.device()->seek(sectionTableOffset)) {
447
0
        return false;
448
0
    }
449
450
13.1M
    for (int i = 0; i < fileHeader.numSections; i++) {
451
13.1M
        PeSection section;
452
13.1M
        ds >> section;
453
13.1M
        sections.append(section);
454
13.1M
    }
455
456
    // Find resource directory.
457
1.97k
    qint64 dataDirOffset = dosHeader.newHeaderOffset;
458
1.97k
    if (isPe32Plus) {
459
1.54k
        dataDirOffset += PeOffsetToDataDirectoryPe32Plus;
460
1.54k
    } else {
461
436
        dataDirOffset += PeOffsetToDataDirectoryPe32;
462
436
    }
463
1.97k
    dataDirOffset += qint64(PeDataDirectoryIndex::Resource) * PeDataDirectorySize;
464
1.97k
    if (!ds.device()->seek(dataDirOffset)) {
465
0
        return false;
466
0
    }
467
1.97k
    PeDataDirectory resourceDirectory;
468
1.97k
    ds >> resourceDirectory;
469
470
    // Read resource tree.
471
1.97k
    auto resourceOffset = addressToOffset(sections, resourceDirectory.virtualAddress);
472
1.97k
    if (resourceOffset < 0) {
473
287
        return false;
474
287
    }
475
476
1.69k
    if (!ds.device()->seek(resourceOffset)) {
477
0
        return false;
478
0
    }
479
480
1.69k
    const auto level1 = readResourceDataDirectoryEntry(ds);
481
482
9.58M
    for (auto entry1 : level1) {
483
9.58M
        if ((entry1.offset & PeSubdirBitMask) == 0)
484
9.57M
            continue;
485
9.17k
        if (!ds.device()->seek(resourceOffset + (entry1.offset & ~PeSubdirBitMask))) {
486
0
            return false;
487
0
        }
488
489
9.17k
        const auto level2 = readResourceDataDirectoryEntry(ds);
490
491
13.4M
        for (auto entry2 : level2) {
492
13.4M
            if ((entry2.offset & PeSubdirBitMask) == 0)
493
13.4M
                continue;
494
48.6k
            if (!ds.device()->seek(resourceOffset + (entry2.offset & ~PeSubdirBitMask))) {
495
0
                return false;
496
0
            }
497
498
            // Read subdirectory.
499
48.6k
            const auto level3 = readResourceDataDirectoryEntry(ds);
500
501
18.2M
            for (auto entry3 : level3) {
502
18.2M
                if ((entry3.offset & PeSubdirBitMask) == PeSubdirBitMask)
503
143k
                    continue;
504
18.0M
                if (!ds.device()->seek(resourceOffset + (entry3.offset & ~PeSubdirBitMask))) {
505
0
                    return false;
506
0
                }
507
508
                // Read data.
509
18.0M
                PeResourceDataEntry dataEntry;
510
18.0M
                ds >> dataEntry;
511
512
18.0M
                switch (ResourceType(entry1.resourceId)) {
513
6.09M
                case ResourceType::Icon:
514
6.09M
                    iconResources[entry2.resourceId] = dataEntry;
515
6.09M
                    break;
516
517
4.18M
                case ResourceType::GroupIcon:
518
4.18M
                    if (!primaryIconGroupResource.has_value()) {
519
1.26k
                        primaryIconGroupResource = dataEntry;
520
1.26k
                    }
521
18.0M
                }
522
18.0M
            }
523
48.6k
        }
524
9.17k
    }
525
526
1.69k
    if (!primaryIconGroupResource.has_value()) {
527
430
        return false;
528
430
    }
529
530
1.26k
    if (!ds.device()->seek(addressToOffset(sections, primaryIconGroupResource->dataAddress))) {
531
170
        return false;
532
170
    }
533
534
1.09k
    RtGroupIconDirectory primaryIconGroup;
535
1.09k
    ds >> primaryIconGroup;
536
537
1.09k
    IconDir icoFileHeader;
538
1.09k
    icoFileHeader.reserved = 0;
539
1.09k
    icoFileHeader.type = 1; // Always 1 for ico files.
540
1.09k
    icoFileHeader.count = primaryIconGroup.count;
541
1.09k
    out << icoFileHeader;
542
543
1.09k
    quint32 dataOffset = IconDirSize + IconDirEntrySize * primaryIconGroup.count;
544
1.09k
    QVector<QPair<qint64, quint32>> resourceOffsetSizePairs;
545
546
6.35M
    for (int i = 0; i < primaryIconGroup.count; i++) {
547
6.35M
        RtGroupIconDirectoryEntry entry;
548
6.35M
        ds >> entry;
549
550
6.35M
        IconDirEntry icoFileEntry{entry, dataOffset};
551
6.35M
        out << icoFileEntry;
552
553
6.35M
        if (auto it = iconResources.find(entry.resourceId); it != iconResources.end()) {
554
6.35M
            PeResourceDataEntry iconResource = *it;
555
6.35M
            resourceOffsetSizePairs.append({addressToOffset(sections, iconResource.dataAddress), iconResource.size});
556
6.35M
            dataOffset += iconResource.size;
557
6.35M
        } else {
558
194
            return false;
559
194
        }
560
6.35M
    }
561
562
6.00M
    for (auto offsetSizePair : resourceOffsetSizePairs) {
563
6.00M
        if (!ds.device()->seek(offsetSizePair.first)) {
564
37
            return false;
565
37
        }
566
6.00M
        outputDevice->write(ds.device()->read(offsetSizePair.second));
567
6.00M
    }
568
569
861
    return true;
570
898
}
571
572
}
573
574
bool ExeUtils::loadIcoDataFromExe(QIODevice *inputDevice, QIODevice *outputDevice)
575
4.56k
{
576
4.56k
    QDataStream ds{inputDevice};
577
4.56k
    ds.setByteOrder(QDataStream::LittleEndian);
578
579
    // Read DOS header.
580
4.56k
    DosHeader dosHeader;
581
4.56k
    ds >> dosHeader;
582
583
    // Verify the MZ header.
584
4.56k
    if (dosHeader.signature[0] != 'M' || dosHeader.signature[1] != 'Z') {
585
488
        return false;
586
488
    }
587
588
4.08k
    if (readPortableExecutablePrimaryIcon(ds, dosHeader, outputDevice)) {
589
861
        return true;
590
861
    }
591
592
3.21k
    if (readNewExecutablePrimaryIcon(ds, dosHeader, outputDevice)) {
593
955
        return true;
594
955
    }
595
596
2.26k
    return false;
597
3.21k
}