Coverage Report

Created: 2026-09-14 07:15

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/serenity/Userland/Libraries/LibGfx/ImageFormats/ICOLoader.cpp
Line
Count
Source
1
/*
2
 * Copyright (c) 2020, Paul Roukema <roukemap@gmail.com>
3
 *
4
 * SPDX-License-Identifier: BSD-2-Clause
5
 */
6
7
#include <AK/ByteBuffer.h>
8
#include <AK/Debug.h>
9
#include <AK/Enumerate.h>
10
#include <AK/MemoryStream.h>
11
#include <AK/Types.h>
12
#include <LibGfx/ImageFormats/BMPLoader.h>
13
#include <LibGfx/ImageFormats/ICOLoader.h>
14
#include <LibGfx/ImageFormats/PNGLoader.h>
15
#include <string.h>
16
17
namespace Gfx {
18
19
// FIXME: This is in little-endian order. Maybe need a NetworkOrdered<T> equivalent eventually.
20
struct ICONDIR {
21
    u16 must_be_0 = 0;
22
    u16 must_be_1 = 0;
23
    u16 image_count = 0;
24
};
25
static_assert(AssertSize<ICONDIR, 6>());
26
27
struct ICONDIRENTRY {
28
    u8 width;
29
    u8 height;
30
    u8 color_count;
31
    u8 reserved_0;
32
    u16 planes;
33
    u16 bits_per_pixel;
34
    u32 size;
35
    u32 offset;
36
};
37
static_assert(AssertSize<ICONDIRENTRY, 16>());
38
39
};
40
41
template<>
42
class AK::Traits<Gfx::ICONDIR> : public DefaultTraits<Gfx::ICONDIR> {
43
public:
44
0
    static constexpr bool is_trivially_serializable() { return true; }
45
};
46
47
template<>
48
class AK::Traits<Gfx::ICONDIRENTRY> : public DefaultTraits<Gfx::ICONDIRENTRY> {
49
public:
50
0
    static constexpr bool is_trivially_serializable() { return true; }
51
};
52
53
namespace Gfx {
54
55
struct ICOImageDescriptor {
56
    u16 width;
57
    u16 height;
58
    u16 bits_per_pixel;
59
    size_t offset;
60
    size_t size;
61
    RefPtr<Gfx::Bitmap> bitmap;
62
};
63
64
struct ICOLoadingContext {
65
    enum State {
66
        NotDecoded = 0,
67
        Error,
68
        DirectoryDecoded,
69
        BitmapDecoded
70
    };
71
    State state { NotDecoded };
72
    u8 const* data { nullptr };
73
    size_t data_size { 0 };
74
    Vector<ICOImageDescriptor> images;
75
    size_t largest_index;
76
};
77
78
static ErrorOr<size_t> decode_ico_header(Stream& stream)
79
0
{
80
0
    auto header = TRY(stream.read_value<ICONDIR>());
81
0
    if (header.must_be_0 != 0 || header.must_be_1 != 1)
82
0
        return Error::from_string_literal("Invalid ICO header");
83
0
    return { header.image_count };
84
0
}
85
86
static ErrorOr<ICOImageDescriptor> decode_ico_direntry(Stream& stream)
87
0
{
88
0
    auto entry = TRY(stream.read_value<ICONDIRENTRY>());
89
0
    ICOImageDescriptor desc = { entry.width, entry.height, entry.bits_per_pixel, entry.offset, entry.size, nullptr };
90
0
    if (desc.width == 0)
91
0
        desc.width = 256;
92
0
    if (desc.height == 0)
93
0
        desc.height = 256;
94
95
0
    return { desc };
96
0
}
97
98
static size_t find_largest_image(ICOLoadingContext const& context)
99
0
{
100
0
    size_t max_area = 0;
101
0
    size_t largest_index = 0;
102
0
    u16 max_bits_per_pixel = 0;
103
104
0
    for (auto const& [index, desc] : enumerate(context.images)) {
105
0
        auto area = static_cast<size_t>(desc.width) * static_cast<size_t>(desc.height);
106
107
0
        if (area > max_area || (area == max_area && desc.bits_per_pixel > max_bits_per_pixel)) {
108
0
            max_area = area;
109
0
            largest_index = index;
110
0
            max_bits_per_pixel = desc.bits_per_pixel;
111
0
        }
112
0
    }
113
114
0
    return largest_index;
115
0
}
116
117
static ErrorOr<void> load_ico_directory(ICOLoadingContext& context)
118
0
{
119
0
    FixedMemoryStream stream { { context.data, context.data_size } };
120
121
0
    auto image_count = TRY(decode_ico_header(stream));
122
0
    if (image_count == 0)
123
0
        return Error::from_string_literal("ICO file has no images");
124
125
0
    for (size_t i = 0; i < image_count; ++i) {
126
0
        auto desc = TRY(decode_ico_direntry(stream));
127
0
        if (desc.offset + desc.size < desc.offset // detect integer overflow
128
0
            || (desc.offset + desc.size) > context.data_size) {
129
0
            dbgln_if(ICO_DEBUG, "load_ico_directory: offset: {} size: {} doesn't fit in ICO size: {}", desc.offset, desc.size, context.data_size);
130
0
            return Error::from_string_literal("ICO size too large");
131
0
        }
132
0
        dbgln_if(ICO_DEBUG, "load_ico_directory: index {} width: {} height: {} offset: {} size: {}", i, desc.width, desc.height, desc.offset, desc.size);
133
0
        TRY(context.images.try_append(desc));
134
0
    }
135
0
    context.largest_index = find_largest_image(context);
136
0
    context.state = ICOLoadingContext::State::DirectoryDecoded;
137
0
    return {};
138
0
}
139
140
ErrorOr<void> ICOImageDecoderPlugin::load_ico_bitmap(ICOLoadingContext& context)
141
0
{
142
0
    VERIFY(context.state >= ICOLoadingContext::State::DirectoryDecoded);
143
144
0
    size_t const real_index = context.largest_index;
145
0
    if (real_index >= context.images.size())
146
0
        return Error::from_string_literal("Index out of bounds");
147
148
0
    ICOImageDescriptor& desc = context.images[real_index];
149
0
    if (PNGImageDecoderPlugin::sniff({ context.data + desc.offset, desc.size })) {
150
0
        auto png_decoder = TRY(PNGImageDecoderPlugin::create({ context.data + desc.offset, desc.size }));
151
0
        auto decoded_png_frame = TRY(png_decoder->frame(0));
152
0
        if (!decoded_png_frame.image) {
153
0
            dbgln_if(ICO_DEBUG, "load_ico_bitmap: failed to load PNG encoded image index: {}", real_index);
154
0
            return Error::from_string_literal("Encoded image not null");
155
0
        }
156
0
        desc.bitmap = decoded_png_frame.image;
157
0
        return {};
158
0
    } else {
159
0
        auto bmp_decoder = TRY(BMPImageDecoderPlugin::create_as_included_in_ico({}, { context.data + desc.offset, desc.size }));
160
        // NOTE: We don't initialize a BMP decoder in the usual way, but rather
161
        // we just create an object and try to sniff for a frame when it's included
162
        // inside an ICO image.
163
0
        if (bmp_decoder->sniff_dib()) {
164
0
            auto decoded_bmp_frame = TRY(bmp_decoder->frame(0));
165
0
            if (!decoded_bmp_frame.image) {
166
0
                dbgln_if(ICO_DEBUG, "load_ico_bitmap: failed to load BMP encoded image index: {}", real_index);
167
0
                return Error::from_string_literal("Encoded image not null");
168
0
            }
169
0
            desc.bitmap = decoded_bmp_frame.image;
170
0
        } else {
171
0
            dbgln_if(ICO_DEBUG, "load_ico_bitmap: encoded image not supported at index: {}", real_index);
172
0
            return Error::from_string_literal("Encoded image not supported");
173
0
        }
174
0
        return {};
175
0
    }
176
0
}
177
178
bool ICOImageDecoderPlugin::sniff(ReadonlyBytes data)
179
0
{
180
0
    FixedMemoryStream stream { data };
181
0
    return !decode_ico_header(stream).is_error();
182
0
}
183
184
ErrorOr<NonnullOwnPtr<ImageDecoderPlugin>> ICOImageDecoderPlugin::create(ReadonlyBytes data)
185
0
{
186
0
    auto plugin = TRY(adopt_nonnull_own_or_enomem(new (nothrow) ICOImageDecoderPlugin(data.data(), data.size())));
187
0
    TRY(load_ico_directory(*plugin->m_context));
188
0
    return plugin;
189
0
}
190
191
ICOImageDecoderPlugin::ICOImageDecoderPlugin(u8 const* data, size_t size)
192
0
{
193
0
    m_context = make<ICOLoadingContext>();
194
0
    m_context->data = data;
195
0
    m_context->data_size = size;
196
0
}
197
198
0
ICOImageDecoderPlugin::~ICOImageDecoderPlugin() = default;
199
200
IntSize ICOImageDecoderPlugin::size()
201
0
{
202
0
    return { m_context->images[m_context->largest_index].width, m_context->images[m_context->largest_index].height };
203
0
}
204
205
ErrorOr<ImageFrameDescriptor> ICOImageDecoderPlugin::frame(size_t index, Optional<IntSize>)
206
0
{
207
0
    if (index > 0)
208
0
        return Error::from_string_literal("ICOImageDecoderPlugin: Invalid frame index");
209
210
0
    if (m_context->state == ICOLoadingContext::State::Error)
211
0
        return Error::from_string_literal("ICOImageDecoderPlugin: Decoding failed");
212
213
0
    if (m_context->state < ICOLoadingContext::State::BitmapDecoded) {
214
        // NOTE: This forces the chunk decoding to happen.
215
0
        auto maybe_error = load_ico_bitmap(*m_context);
216
0
        if (maybe_error.is_error()) {
217
0
            m_context->state = ICOLoadingContext::State::Error;
218
0
            return Error::from_string_literal("ICOImageDecoderPlugin: Decoding failed");
219
0
        }
220
0
        m_context->state = ICOLoadingContext::State::BitmapDecoded;
221
0
    }
222
223
0
    VERIFY(m_context->images[m_context->largest_index].bitmap);
224
0
    return ImageFrameDescriptor { m_context->images[m_context->largest_index].bitmap, 0 };
225
0
}
226
227
}