Coverage Report

Created: 2026-04-12 06:58

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/qpdf/libqpdf/Pl_DCT.cc
Line
Count
Source
1
#include <qpdf/Pl_DCT.hh>
2
3
#include <qpdf/QIntC.hh>
4
#include <qpdf/QTC.hh>
5
#include <qpdf/Util.hh>
6
#include <qpdf/global_private.hh>
7
8
#include <csetjmp>
9
#include <stdexcept>
10
#include <string>
11
12
#if BITS_IN_JSAMPLE != 8
13
# error "qpdf does not support libjpeg built with BITS_IN_JSAMPLE != 8"
14
#endif
15
16
using namespace qpdf;
17
18
namespace
19
{
20
    class FunctionCallbackConfig: public Pl_DCT::CompressConfig
21
    {
22
      public:
23
        explicit FunctionCallbackConfig(std::function<void(jpeg_compress_struct*)> f) :
24
0
            f(std::move(f))
25
0
        {
26
0
        }
27
0
        ~FunctionCallbackConfig() override = default;
28
        void
29
        apply(jpeg_compress_struct* config) override
30
0
        {
31
0
            f(config);
32
0
        };
33
34
        std::function<void(jpeg_compress_struct*)> f;
35
    };
36
37
    struct qpdf_jpeg_error_mgr
38
    {
39
        struct jpeg_error_mgr pub;
40
        jmp_buf jmpbuf;
41
        std::string msg;
42
    };
43
44
    long const& memory_limit{global::Limits::dct_max_memory()};
45
    int const& scan_limit{global::Limits::dct_max_progressive_scans()};
46
    bool const& throw_on_corrupt_data{global::Options::dct_throw_on_corrupt_data()};
47
} // namespace
48
49
static void
50
error_handler(j_common_ptr cinfo)
51
1.54k
{
52
1.54k
    auto* jerr = reinterpret_cast<qpdf_jpeg_error_mgr*>(cinfo->err);
53
1.54k
    char buf[JMSG_LENGTH_MAX];
54
1.54k
    (*cinfo->err->format_message)(cinfo, buf);
55
1.54k
    jerr->msg = buf;
56
1.54k
    longjmp(jerr->jmpbuf, 1);
57
1.54k
}
58
59
static void
60
emit_message(j_common_ptr cinfo, int msg_level)
61
152k
{
62
152k
    if (msg_level == -1) {
63
2.12k
        auto* jerr = reinterpret_cast<qpdf_jpeg_error_mgr*>(cinfo->err);
64
2.12k
        jerr->msg = "Pl_DCT::decompress: JPEG data is corrupt";
65
2.12k
        longjmp(jerr->jmpbuf, 1);
66
2.12k
    }
67
152k
}
68
69
static void
70
progress_monitor(j_common_ptr cinfo)
71
8.64M
{
72
8.64M
    if (cinfo->is_decompressor &&
73
8.64M
        reinterpret_cast<jpeg_decompress_struct*>(cinfo)->input_scan_number > scan_limit) {
74
2
        auto* jerr = reinterpret_cast<qpdf_jpeg_error_mgr*>(cinfo->err);
75
2
        jerr->msg = "Pl_DCT::decompress: JPEG data has too many scans";
76
2
        longjmp(jerr->jmpbuf, 1);
77
2
    }
78
8.64M
}
79
80
class Pl_DCT::Members
81
{
82
  public:
83
    // For compression
84
    Members(
85
        JDIMENSION image_width,
86
        JDIMENSION image_height,
87
        int components,
88
        J_COLOR_SPACE color_space,
89
        CompressConfig* config_callback) :
90
0
        action(a_compress),
91
0
        image_width(image_width),
92
0
        image_height(image_height),
93
0
        components(components),
94
0
        color_space(color_space),
95
0
        config_callback(config_callback)
96
0
    {
97
0
    }
98
99
    // For decompression
100
    Members() :
101
7.06k
        action(a_decompress)
102
7.06k
    {
103
7.06k
    }
104
105
    Members(Members const&) = delete;
106
107
7.06k
    ~Members() = default;
108
109
    action_e action;
110
    std::string buf;
111
112
    // Used for compression
113
    JDIMENSION image_width{0};
114
    JDIMENSION image_height{0};
115
    int components{1};
116
    J_COLOR_SPACE color_space{JCS_GRAYSCALE};
117
118
    CompressConfig* config_callback{nullptr};
119
};
120
121
Pl_DCT::Pl_DCT(char const* identifier, Pipeline* next) :
122
7.06k
    Pipeline(identifier, next),
123
7.06k
    m(std::make_unique<Members>())
124
7.06k
{
125
7.06k
    util::assertion(next, "Attempt to create Pl_DCT with nullptr as next");
126
7.06k
}
127
128
void
129
Pl_DCT::setMemoryLimit(long limit)
130
7.06k
{
131
7.06k
    global::Limits::dct_max_memory(limit);
132
7.06k
}
133
134
void
135
Pl_DCT::setScanLimit(int limit)
136
7.06k
{
137
7.06k
    global::Limits::dct_max_progressive_scans(limit);
138
7.06k
}
139
140
void
141
Pl_DCT::setThrowOnCorruptData(bool treat_as_error)
142
7.06k
{
143
7.06k
    global::options::dct_throw_on_corrupt_data(treat_as_error);
144
7.06k
}
145
146
Pl_DCT::Pl_DCT(
147
    char const* identifier,
148
    Pipeline* next,
149
    JDIMENSION image_width,
150
    JDIMENSION image_height,
151
    int components,
152
    J_COLOR_SPACE color_space,
153
    CompressConfig* compress_callback) :
154
0
    Pipeline(identifier, next),
155
0
    m(std::make_unique<Members>(
156
0
        image_width, image_height, components, color_space, compress_callback))
157
0
{
158
0
}
159
160
// Must be explicit and not inline -- see QPDF_DLL_CLASS in README-maintainer
161
7.06k
Pl_DCT::~Pl_DCT() = default;
162
163
void
164
Pl_DCT::write(unsigned char const* data, size_t len)
165
7.06k
{
166
7.06k
    if (len > 0) {
167
7.06k
        m->buf.append(reinterpret_cast<char const*>(data), len);
168
7.06k
    }
169
7.06k
}
170
171
void
172
Pl_DCT::finish()
173
7.06k
{
174
7.06k
    if (m->buf.empty()) {
175
        // Special case: empty data will never succeed and probably means we're calling finish a
176
        // second time from an exception handler.
177
0
        next()->finish();
178
0
        return;
179
0
    }
180
181
7.06k
    struct jpeg_compress_struct cinfo_compress;
182
7.06k
    struct jpeg_decompress_struct cinfo_decompress;
183
7.06k
    struct qpdf_jpeg_error_mgr jerr;
184
185
7.06k
    cinfo_compress.err = jpeg_std_error(&(jerr.pub));
186
7.06k
    cinfo_decompress.err = jpeg_std_error(&(jerr.pub));
187
7.06k
    jerr.pub.error_exit = error_handler;
188
7.06k
    if (m->action == a_decompress && throw_on_corrupt_data) {
189
7.06k
        jerr.pub.emit_message = emit_message;
190
7.06k
    }
191
192
7.06k
    bool error = false;
193
    // The jpeg library is a "C" library, so we use setjmp and longjmp for exception handling.
194
7.06k
    if (setjmp(jerr.jmpbuf) == 0) {
195
7.06k
        try {
196
7.06k
            if (m->action == a_compress) {
197
0
                compress(reinterpret_cast<void*>(&cinfo_compress));
198
7.06k
            } else {
199
7.06k
                decompress(reinterpret_cast<void*>(&cinfo_decompress));
200
7.06k
            }
201
7.06k
        } catch (std::exception& e) {
202
            // Convert an exception back to a longjmp so we can ensure that the right cleanup
203
            // happens. This will get converted back to an exception.
204
2.74k
            jerr.msg = e.what();
205
2.74k
            longjmp(jerr.jmpbuf, 1);
206
2.74k
        }
207
7.06k
    } else {
208
0
        error = true;
209
0
    }
210
211
651
    if (m->action == a_compress) {
212
0
        jpeg_destroy_compress(&cinfo_compress);
213
0
    }
214
7.06k
    if (m->action == a_decompress) {
215
7.06k
        jpeg_destroy_decompress(&cinfo_decompress);
216
7.06k
    }
217
6.41k
    if (error) {
218
6.41k
        throw std::runtime_error(jerr.msg);
219
6.41k
    }
220
651
}
221
222
namespace
223
{
224
    struct dct_pipeline_dest
225
    {
226
        struct jpeg_destination_mgr pub; /* public fields */
227
        unsigned char* buffer;
228
        size_t size;
229
        Pipeline* next;
230
    };
231
} // namespace
232
233
static void
234
init_pipeline_destination(j_compress_ptr)
235
0
{
236
0
}
237
238
static boolean
239
empty_pipeline_output_buffer(j_compress_ptr cinfo)
240
0
{
241
0
    QTC::TC("libtests", "Pl_DCT empty_pipeline_output_buffer");
242
0
    auto* dest = reinterpret_cast<dct_pipeline_dest*>(cinfo->dest);
243
0
    dest->next->write(dest->buffer, dest->size);
244
0
    dest->pub.next_output_byte = dest->buffer;
245
0
    dest->pub.free_in_buffer = dest->size;
246
0
    return TRUE;
247
0
}
248
249
static void
250
term_pipeline_destination(j_compress_ptr cinfo)
251
0
{
252
0
    QTC::TC("libtests", "Pl_DCT term_pipeline_destination");
253
0
    auto* dest = reinterpret_cast<dct_pipeline_dest*>(cinfo->dest);
254
0
    dest->next->write(dest->buffer, dest->size - dest->pub.free_in_buffer);
255
0
}
256
257
static void
258
jpeg_pipeline_dest(j_compress_ptr cinfo, unsigned char* outbuffer, size_t size, Pipeline* next)
259
0
{
260
0
    cinfo->dest = static_cast<struct jpeg_destination_mgr*>(
261
        // line-break
262
0
        (*cinfo->mem->alloc_small)(
263
0
            reinterpret_cast<j_common_ptr>(cinfo), JPOOL_PERMANENT, sizeof(dct_pipeline_dest)));
264
0
    auto* dest = reinterpret_cast<dct_pipeline_dest*>(cinfo->dest);
265
0
    dest->pub.init_destination = init_pipeline_destination;
266
0
    dest->pub.empty_output_buffer = empty_pipeline_output_buffer;
267
0
    dest->pub.term_destination = term_pipeline_destination;
268
0
    dest->pub.next_output_byte = dest->buffer = outbuffer;
269
0
    dest->pub.free_in_buffer = dest->size = size;
270
0
    dest->next = next;
271
0
}
272
273
static void
274
init_buffer_source(j_decompress_ptr)
275
7.06k
{
276
7.06k
}
277
278
static boolean
279
fill_buffer_input_buffer(j_decompress_ptr)
280
2.72k
{
281
    // The whole JPEG data is expected to reside in the supplied memory buffer, so any request for
282
    // more data beyond the given buffer size is treated as an error.
283
2.72k
    throw std::runtime_error("invalid jpeg data reading from buffer");
284
0
    return TRUE;
285
2.72k
}
286
287
static void
288
skip_buffer_input_data(j_decompress_ptr cinfo, long num_bytes)
289
3.38k
{
290
3.38k
    util::no_ci_rt_error_if(
291
3.38k
        num_bytes < 0, "reading jpeg: jpeg library requested skipping a negative number of bytes");
292
3.38k
    size_t to_skip = QIntC::to_size(num_bytes);
293
3.38k
    if (to_skip > 0 && to_skip <= cinfo->src->bytes_in_buffer) {
294
3.24k
        cinfo->src->next_input_byte += to_skip;
295
3.24k
        cinfo->src->bytes_in_buffer -= to_skip;
296
3.24k
    } else if (to_skip != 0) {
297
141
        cinfo->src->next_input_byte += cinfo->src->bytes_in_buffer;
298
141
        cinfo->src->bytes_in_buffer = 0;
299
141
    }
300
3.38k
}
301
302
static void
303
term_buffer_source(j_decompress_ptr)
304
651
{
305
651
}
306
307
static void
308
jpeg_buffer_src(j_decompress_ptr cinfo, std::string& buffer)
309
7.06k
{
310
7.06k
    cinfo->src = reinterpret_cast<jpeg_source_mgr*>(
311
        // line-break
312
7.06k
        (*cinfo->mem->alloc_small)(
313
7.06k
            reinterpret_cast<j_common_ptr>(cinfo), JPOOL_PERMANENT, sizeof(jpeg_source_mgr)));
314
315
7.06k
    jpeg_source_mgr* src = cinfo->src;
316
7.06k
    src->init_source = init_buffer_source;
317
7.06k
    src->fill_input_buffer = fill_buffer_input_buffer;
318
7.06k
    src->skip_input_data = skip_buffer_input_data;
319
7.06k
    src->resync_to_restart = jpeg_resync_to_restart; /* use default method */
320
7.06k
    src->term_source = term_buffer_source;
321
7.06k
    src->bytes_in_buffer = buffer.size();
322
7.06k
    src->next_input_byte = reinterpret_cast<unsigned char*>(buffer.data());
323
7.06k
}
324
325
void
326
Pl_DCT::compress(void* cinfo_p)
327
0
{
328
0
    auto* cinfo = reinterpret_cast<jpeg_compress_struct*>(cinfo_p);
329
330
0
#if ((defined(__GNUC__) && ((__GNUC__ * 100) + __GNUC_MINOR__) >= 406) || defined(__clang__))
331
0
# pragma GCC diagnostic push
332
0
# pragma GCC diagnostic ignored "-Wold-style-cast"
333
0
#endif
334
0
    jpeg_create_compress(cinfo);
335
0
#if ((defined(__GNUC__) && ((__GNUC__ * 100) + __GNUC_MINOR__) >= 406) || defined(__clang__))
336
0
# pragma GCC diagnostic pop
337
0
#endif
338
0
    static int const BUF_SIZE = 65536;
339
0
    auto outbuffer_ph = std::make_unique<unsigned char[]>(BUF_SIZE);
340
0
    unsigned char* outbuffer = outbuffer_ph.get();
341
0
    jpeg_pipeline_dest(cinfo, outbuffer, BUF_SIZE, next());
342
343
0
    cinfo->image_width = m->image_width;
344
0
    cinfo->image_height = m->image_height;
345
0
    cinfo->input_components = m->components;
346
0
    cinfo->in_color_space = m->color_space;
347
0
    jpeg_set_defaults(cinfo);
348
0
    if (m->config_callback) {
349
0
        m->config_callback->apply(cinfo);
350
0
    }
351
352
0
    jpeg_start_compress(cinfo, TRUE);
353
354
0
    unsigned int width = cinfo->image_width * QIntC::to_uint(cinfo->input_components);
355
0
    size_t expected_size = QIntC::to_size(cinfo->image_height) *
356
0
        QIntC::to_size(cinfo->image_width) * QIntC::to_size(cinfo->input_components);
357
0
    util::no_ci_rt_error_if(
358
0
        m->buf.size() != expected_size,
359
0
        "Pl_DCT: image buffer size = " + std::to_string(m->buf.size()) +
360
0
            "; expected size = " + std::to_string(expected_size));
361
0
    JSAMPROW row_pointer[1];
362
0
    auto buffer = reinterpret_cast<unsigned char*>(m->buf.data());
363
0
    while (cinfo->next_scanline < cinfo->image_height) {
364
        // We already verified that the buffer is big enough.
365
0
        row_pointer[0] = &buffer[cinfo->next_scanline * width];
366
0
        (void)jpeg_write_scanlines(cinfo, row_pointer, 1);
367
0
    }
368
0
    jpeg_finish_compress(cinfo);
369
0
    next()->finish();
370
0
}
371
372
void
373
Pl_DCT::decompress(void* cinfo_p)
374
7.06k
{
375
7.06k
    auto* cinfo = reinterpret_cast<jpeg_decompress_struct*>(cinfo_p);
376
377
7.06k
#if ((defined(__GNUC__) && ((__GNUC__ * 100) + __GNUC_MINOR__) >= 406) || defined(__clang__))
378
7.06k
# pragma GCC diagnostic push
379
7.06k
# pragma GCC diagnostic ignored "-Wold-style-cast"
380
7.06k
#endif
381
7.06k
    jpeg_create_decompress(cinfo);
382
7.06k
#if ((defined(__GNUC__) && ((__GNUC__ * 100) + __GNUC_MINOR__) >= 406) || defined(__clang__))
383
7.06k
# pragma GCC diagnostic pop
384
7.06k
#endif
385
386
7.06k
    if (memory_limit > 0) {
387
7.06k
        cinfo->mem->max_memory_to_use = memory_limit;
388
7.06k
    }
389
390
7.06k
    jpeg_buffer_src(cinfo, m->buf);
391
392
7.06k
    (void)jpeg_read_header(cinfo, TRUE);
393
7.06k
    jpeg_calc_output_dimensions(cinfo);
394
7.06k
    unsigned int width = cinfo->output_width * QIntC::to_uint(cinfo->output_components);
395
7.06k
    if (memory_limit > 0 &&
396
5.33k
        width > (static_cast<unsigned long>(memory_limit / 20) / cinfo->output_height)) {
397
        // Even if jpeglib does not run out of memory, qpdf will while buffering the data before
398
        // writing it. Furthermore, for very large images runtime can be significant before the
399
        // first warning is encountered causing a timeout in oss-fuzz.
400
16
        throw std::runtime_error("Pl_DCT::decompress: JPEG data large - may be too slow");
401
16
    }
402
7.05k
    jpeg_progress_mgr progress_mgr;
403
7.05k
    if (scan_limit > 0) {
404
5.31k
        progress_mgr.progress_monitor = &progress_monitor;
405
5.31k
        cinfo->progress = &progress_mgr;
406
5.31k
    }
407
7.05k
    JSAMPARRAY buffer =
408
7.05k
        (*cinfo->mem->alloc_sarray)(reinterpret_cast<j_common_ptr>(cinfo), JPOOL_IMAGE, width, 1);
409
410
7.05k
    (void)jpeg_start_decompress(cinfo);
411
7.24M
    while (cinfo->output_scanline < cinfo->output_height) {
412
7.23M
        (void)jpeg_read_scanlines(cinfo, buffer, 1);
413
7.23M
        next()->write(buffer[0], width * sizeof(buffer[0][0]));
414
7.23M
    }
415
7.05k
    (void)jpeg_finish_decompress(cinfo);
416
7.05k
    next()->finish();
417
7.05k
}
418
419
std::unique_ptr<Pl_DCT::CompressConfig>
420
Pl_DCT::make_compress_config(std::function<void(jpeg_compress_struct*)> f)
421
0
{
422
0
    return std::make_unique<FunctionCallbackConfig>(f);
423
0
}