Coverage Report

Created: 2026-03-12 07:14

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