Coverage Report

Created: 2025-06-22 06:27

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