Coverage Report

Created: 2025-11-24 06:51

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