Coverage Report

Created: 2025-08-26 07:07

/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
1.76k
{
48
1.76k
    auto* jerr = reinterpret_cast<qpdf_jpeg_error_mgr*>(cinfo->err);
49
1.76k
    char buf[JMSG_LENGTH_MAX];
50
1.76k
    (*cinfo->err->format_message)(cinfo, buf);
51
1.76k
    jerr->msg = buf;
52
1.76k
    longjmp(jerr->jmpbuf, 1);
53
1.76k
}
54
55
static void
56
emit_message(j_common_ptr cinfo, int msg_level)
57
123k
{
58
123k
    if (msg_level == -1) {
59
2.66k
        auto* jerr = reinterpret_cast<qpdf_jpeg_error_mgr*>(cinfo->err);
60
2.66k
        jerr->msg = "Pl_DCT::decompress: JPEG data is corrupt";
61
2.66k
        longjmp(jerr->jmpbuf, 1);
62
2.66k
    }
63
123k
}
64
65
static void
66
progress_monitor(j_common_ptr cinfo)
67
8.46M
{
68
8.46M
    if (cinfo->is_decompressor &&
69
8.46M
        reinterpret_cast<jpeg_decompress_struct*>(cinfo)->input_scan_number > scan_limit) {
70
1
        auto* jerr = reinterpret_cast<qpdf_jpeg_error_mgr*>(cinfo->err);
71
1
        jerr->msg = "Pl_DCT::decompress: JPEG data has too many scans";
72
1
        longjmp(jerr->jmpbuf, 1);
73
1
    }
74
8.46M
}
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
        image_width(image_width),
88
0
        image_height(image_height),
89
0
        components(components),
90
0
        color_space(color_space),
91
0
        config_callback(config_callback)
92
0
    {
93
0
    }
94
95
    // For decompression
96
    Members() :
97
8.14k
        action(a_decompress)
98
8.14k
    {
99
8.14k
    }
100
101
    Members(Members const&) = delete;
102
103
8.14k
    ~Members() = default;
104
105
    action_e action;
106
    std::string buf;
107
108
    // Used for compression
109
    JDIMENSION image_width{0};
110
    JDIMENSION image_height{0};
111
    int components{1};
112
    J_COLOR_SPACE color_space{JCS_GRAYSCALE};
113
114
    CompressConfig* config_callback{nullptr};
115
};
116
117
Pl_DCT::Pl_DCT(char const* identifier, Pipeline* next) :
118
8.14k
    Pipeline(identifier, next),
119
8.14k
    m(std::make_unique<Members>())
120
8.14k
{
121
8.14k
    if (!next) {
122
0
        throw std::logic_error("Attempt to create Pl_DCT with nullptr as next");
123
0
    }
124
8.14k
}
125
126
void
127
Pl_DCT::setMemoryLimit(long limit)
128
8.14k
{
129
8.14k
    memory_limit = limit;
130
8.14k
}
131
132
void
133
Pl_DCT::setScanLimit(int limit)
134
8.14k
{
135
8.14k
    scan_limit = limit;
136
8.14k
}
137
138
void
139
Pl_DCT::setThrowOnCorruptData(bool treat_as_error)
140
8.14k
{
141
8.14k
    throw_on_corrupt_data = treat_as_error;
142
8.14k
}
143
144
Pl_DCT::Pl_DCT(
145
    char const* identifier,
146
    Pipeline* next,
147
    JDIMENSION image_width,
148
    JDIMENSION image_height,
149
    int components,
150
    J_COLOR_SPACE color_space,
151
    CompressConfig* compress_callback) :
152
0
    Pipeline(identifier, next),
153
0
    m(std::make_unique<Members>(
154
0
        image_width, image_height, components, color_space, compress_callback))
155
0
{
156
0
}
157
158
// Must be explicit and not inline -- see QPDF_DLL_CLASS in README-maintainer
159
8.14k
Pl_DCT::~Pl_DCT() = default;
160
161
void
162
Pl_DCT::write(unsigned char const* data, size_t len)
163
8.14k
{
164
8.14k
    if (len > 0) {
165
8.14k
        m->buf.append(reinterpret_cast<char const*>(data), len);
166
8.14k
    }
167
8.14k
}
168
169
void
170
Pl_DCT::finish()
171
8.14k
{
172
8.14k
    if (m->buf.empty()) {
173
        // Special case: empty data will never succeed and probably means we're calling finish a
174
        // second time from an exception handler.
175
0
        next()->finish();
176
0
        return;
177
0
    }
178
179
8.14k
    struct jpeg_compress_struct cinfo_compress;
180
8.14k
    struct jpeg_decompress_struct cinfo_decompress;
181
8.14k
    struct qpdf_jpeg_error_mgr jerr;
182
183
8.14k
    cinfo_compress.err = jpeg_std_error(&(jerr.pub));
184
8.14k
    cinfo_decompress.err = jpeg_std_error(&(jerr.pub));
185
8.14k
    jerr.pub.error_exit = error_handler;
186
8.14k
    if (m->action == a_decompress && throw_on_corrupt_data) {
187
8.14k
        jerr.pub.emit_message = emit_message;
188
8.14k
    }
189
190
8.14k
    bool error = false;
191
    // The jpeg library is a "C" library, so we use setjmp and longjmp for exception handling.
192
8.14k
    if (setjmp(jerr.jmpbuf) == 0) {
193
8.14k
        try {
194
8.14k
            if (m->action == a_compress) {
195
0
                compress(reinterpret_cast<void*>(&cinfo_compress));
196
8.14k
            } else {
197
8.14k
                decompress(reinterpret_cast<void*>(&cinfo_decompress));
198
8.14k
            }
199
8.14k
        } catch (std::exception& e) {
200
            // Convert an exception back to a longjmp so we can ensure that the right cleanup
201
            // happens. This will get converted back to an exception.
202
3.04k
            jerr.msg = e.what();
203
3.04k
            longjmp(jerr.jmpbuf, 1);
204
3.04k
        }
205
8.14k
    } else {
206
0
        error = true;
207
0
    }
208
209
668
    if (m->action == a_compress) {
210
0
        jpeg_destroy_compress(&cinfo_compress);
211
0
    }
212
8.14k
    if (m->action == a_decompress) {
213
8.14k
        jpeg_destroy_decompress(&cinfo_decompress);
214
8.14k
    }
215
7.47k
    if (error) {
216
7.47k
        throw std::runtime_error(jerr.msg);
217
7.47k
    }
218
668
}
219
220
namespace
221
{
222
    struct dct_pipeline_dest
223
    {
224
        struct jpeg_destination_mgr pub; /* public fields */
225
        unsigned char* buffer;
226
        size_t size;
227
        Pipeline* next;
228
    };
229
} // namespace
230
231
static void
232
init_pipeline_destination(j_compress_ptr)
233
0
{
234
0
}
235
236
static boolean
237
empty_pipeline_output_buffer(j_compress_ptr cinfo)
238
0
{
239
0
    QTC::TC("libtests", "Pl_DCT empty_pipeline_output_buffer");
240
0
    auto* dest = reinterpret_cast<dct_pipeline_dest*>(cinfo->dest);
241
0
    dest->next->write(dest->buffer, dest->size);
242
0
    dest->pub.next_output_byte = dest->buffer;
243
0
    dest->pub.free_in_buffer = dest->size;
244
0
    return TRUE;
245
0
}
246
247
static void
248
term_pipeline_destination(j_compress_ptr cinfo)
249
0
{
250
0
    QTC::TC("libtests", "Pl_DCT term_pipeline_destination");
251
0
    auto* dest = reinterpret_cast<dct_pipeline_dest*>(cinfo->dest);
252
0
    dest->next->write(dest->buffer, dest->size - dest->pub.free_in_buffer);
253
0
}
254
255
static void
256
jpeg_pipeline_dest(j_compress_ptr cinfo, unsigned char* outbuffer, size_t size, Pipeline* next)
257
0
{
258
0
    cinfo->dest = static_cast<struct jpeg_destination_mgr*>(
259
        // line-break
260
0
        (*cinfo->mem->alloc_small)(
261
0
            reinterpret_cast<j_common_ptr>(cinfo), JPOOL_PERMANENT, sizeof(dct_pipeline_dest)));
262
0
    auto* dest = reinterpret_cast<dct_pipeline_dest*>(cinfo->dest);
263
0
    dest->pub.init_destination = init_pipeline_destination;
264
0
    dest->pub.empty_output_buffer = empty_pipeline_output_buffer;
265
0
    dest->pub.term_destination = term_pipeline_destination;
266
0
    dest->pub.next_output_byte = dest->buffer = outbuffer;
267
0
    dest->pub.free_in_buffer = dest->size = size;
268
0
    dest->next = next;
269
0
}
270
271
static void
272
init_buffer_source(j_decompress_ptr)
273
8.14k
{
274
8.14k
}
275
276
static boolean
277
fill_buffer_input_buffer(j_decompress_ptr)
278
3.02k
{
279
    // The whole JPEG data is expected to reside in the supplied memory buffer, so any request for
280
    // more data beyond the given buffer size is treated as an error.
281
3.02k
    throw std::runtime_error("invalid jpeg data reading from buffer");
282
0
    return TRUE;
283
3.02k
}
284
285
static void
286
skip_buffer_input_data(j_decompress_ptr cinfo, long num_bytes)
287
1.80k
{
288
1.80k
    if (num_bytes < 0) {
289
0
        throw std::runtime_error(
290
0
            "reading jpeg: jpeg library requested skipping a negative number of bytes");
291
0
    }
292
1.80k
    size_t to_skip = QIntC::to_size(num_bytes);
293
1.80k
    if ((to_skip > 0) && (to_skip <= cinfo->src->bytes_in_buffer)) {
294
1.65k
        cinfo->src->next_input_byte += to_skip;
295
1.65k
        cinfo->src->bytes_in_buffer -= to_skip;
296
1.65k
    } else if (to_skip != 0) {
297
144
        cinfo->src->next_input_byte += cinfo->src->bytes_in_buffer;
298
144
        cinfo->src->bytes_in_buffer = 0;
299
144
    }
300
1.80k
}
301
302
static void
303
term_buffer_source(j_decompress_ptr)
304
668
{
305
668
}
306
307
static void
308
jpeg_buffer_src(j_decompress_ptr cinfo, std::string& buffer)
309
8.14k
{
310
8.14k
    cinfo->src = reinterpret_cast<jpeg_source_mgr*>(
311
        // line-break
312
8.14k
        (*cinfo->mem->alloc_small)(
313
8.14k
            reinterpret_cast<j_common_ptr>(cinfo), JPOOL_PERMANENT, sizeof(jpeg_source_mgr)));
314
315
8.14k
    jpeg_source_mgr* src = cinfo->src;
316
8.14k
    src->init_source = init_buffer_source;
317
8.14k
    src->fill_input_buffer = fill_buffer_input_buffer;
318
8.14k
    src->skip_input_data = skip_buffer_input_data;
319
8.14k
    src->resync_to_restart = jpeg_resync_to_restart; /* use default method */
320
8.14k
    src->term_source = term_buffer_source;
321
8.14k
    src->bytes_in_buffer = buffer.size();
322
8.14k
    src->next_input_byte = reinterpret_cast<unsigned char*>(buffer.data());
323
8.14k
}
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
    if (m->buf.size() != expected_size) {
358
0
        throw std::runtime_error(
359
0
            "Pl_DCT: image buffer size = " + std::to_string(m->buf.size()) +
360
0
            "; expected size = " + std::to_string(expected_size));
361
0
    }
362
0
    JSAMPROW row_pointer[1];
363
0
    auto buffer = reinterpret_cast<unsigned char*>(m->buf.data());
364
0
    while (cinfo->next_scanline < cinfo->image_height) {
365
        // We already verified that the buffer is big enough.
366
0
        row_pointer[0] = &buffer[cinfo->next_scanline * width];
367
0
        (void)jpeg_write_scanlines(cinfo, row_pointer, 1);
368
0
    }
369
0
    jpeg_finish_compress(cinfo);
370
0
    next()->finish();
371
0
}
372
373
void
374
Pl_DCT::decompress(void* cinfo_p)
375
8.14k
{
376
8.14k
    auto* cinfo = reinterpret_cast<jpeg_decompress_struct*>(cinfo_p);
377
378
8.14k
#if ((defined(__GNUC__) && ((__GNUC__ * 100) + __GNUC_MINOR__) >= 406) || defined(__clang__))
379
8.14k
# pragma GCC diagnostic push
380
8.14k
# pragma GCC diagnostic ignored "-Wold-style-cast"
381
8.14k
#endif
382
8.14k
    jpeg_create_decompress(cinfo);
383
8.14k
#if ((defined(__GNUC__) && ((__GNUC__ * 100) + __GNUC_MINOR__) >= 406) || defined(__clang__))
384
8.14k
# pragma GCC diagnostic pop
385
8.14k
#endif
386
387
8.14k
    if (memory_limit > 0) {
388
8.14k
        cinfo->mem->max_memory_to_use = memory_limit;
389
8.14k
    }
390
391
8.14k
    jpeg_buffer_src(cinfo, m->buf);
392
393
8.14k
    (void)jpeg_read_header(cinfo, TRUE);
394
8.14k
    jpeg_calc_output_dimensions(cinfo);
395
8.14k
    unsigned int width = cinfo->output_width * QIntC::to_uint(cinfo->output_components);
396
8.14k
    if (memory_limit > 0 &&
397
8.14k
        width > (static_cast<unsigned long>(memory_limit) / (20U * cinfo->output_height))) {
398
        // Even if jpeglib does not run out of memory, qpdf will while buffering the data before
399
        // writing it. Furthermore, for very large images runtime can be significant before the
400
        // first warning is encountered causing a timeout in oss-fuzz.
401
19
        throw std::runtime_error("Pl_DCT::decompress: JPEG data large - may be too slow");
402
19
    }
403
8.12k
    jpeg_progress_mgr progress_mgr;
404
8.12k
    if (scan_limit > 0) {
405
6.22k
        progress_mgr.progress_monitor = &progress_monitor;
406
6.22k
        cinfo->progress = &progress_mgr;
407
6.22k
    }
408
8.12k
    JSAMPARRAY buffer =
409
8.12k
        (*cinfo->mem->alloc_sarray)(reinterpret_cast<j_common_ptr>(cinfo), JPOOL_IMAGE, width, 1);
410
411
8.12k
    (void)jpeg_start_decompress(cinfo);
412
6.43M
    while (cinfo->output_scanline < cinfo->output_height) {
413
6.42M
        (void)jpeg_read_scanlines(cinfo, buffer, 1);
414
6.42M
        next()->write(buffer[0], width * sizeof(buffer[0][0]));
415
6.42M
    }
416
8.12k
    (void)jpeg_finish_decompress(cinfo);
417
8.12k
    next()->finish();
418
8.12k
}
419
420
std::unique_ptr<Pl_DCT::CompressConfig>
421
Pl_DCT::make_compress_config(std::function<void(jpeg_compress_struct*)> f)
422
0
{
423
0
    return std::make_unique<FunctionCallbackConfig>(f);
424
0
}