Coverage Report

Created: 2026-08-11 07:29

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/poppler/poppler/OptionalContent.cc
Line
Count
Source
1
//========================================================================
2
//
3
// OptionalContent.cc
4
//
5
// Copyright 2007 Brad Hards <bradh@kde.org>
6
// Copyright 2008 Pino Toscano <pino@kde.org>
7
// Copyright 2008, 2010 Carlos Garcia Campos <carlosgc@gnome.org>
8
// Copyright 2008, 2010, 2011, 2017-2019, 2025, 2026 Albert Astals Cid <aacid@kde.org>
9
// Copyright 2008 Mark Kaplan <mkaplan@finjan.com>
10
// Copyright 2018 Adam Reichold <adam.reichold@t-online.de>
11
// Copyright 2019 Oliver Sander <oliver.sander@tu-dresden.de>
12
// Copyright 2025 g10 Code GmbH, Author: Sune Stolborg Vuorela <sune@vuorela.dk>
13
// Copyright 2026 Adam Sampson <ats@offog.org>
14
//
15
// Released under the GPL (version 2, or later, at your option)
16
//
17
//========================================================================
18
19
#include <config.h>
20
21
#include "goo/GooString.h"
22
#include "Error.h"
23
#include "OptionalContent.h"
24
25
// Max depth of nested visibility expressions.  This is used to catch
26
// infinite loops in the visibility expression object structure.
27
constexpr int visibilityExprRecursionLimit = 50;
28
29
//------------------------------------------------------------------------
30
31
0
OCGs::OCGs(const Object &ocgObject, XRef *xref) : m_xref(xref)
32
0
{
33
    // we need to parse the dictionary here, and build optionalContentGroups
34
0
    ok = true;
35
36
0
    Object ocgs = ocgObject.dictLookup("OCGs");
37
0
    if (!ocgs.isArray()) {
38
0
        error(errSyntaxError, -1, "Expected the optional content group list, but wasn't able to find it, or it isn't an Array");
39
0
        ok = false;
40
0
        return;
41
0
    }
42
0
    Array *ocgsArray = ocgs.getArray();
43
44
    // we now enumerate over the ocgs list, and build up the optionalContentGroups list.
45
0
    for (int i = 0; i < ocgsArray->getLength(); ++i) {
46
0
        Object ocgDict = ocgsArray->get(i);
47
0
        if (!ocgDict.isDict()) {
48
0
            break;
49
0
        }
50
0
        auto thisOptionalContentGroup = std::make_unique<OptionalContentGroup>(ocgDict.getDict());
51
0
        const Object &ocgRef = ocgsArray->getNF(i);
52
0
        if (!ocgRef.isRef()) {
53
0
            break;
54
0
        }
55
0
        thisOptionalContentGroup->setRef(ocgRef.getRef());
56
        // the default is ON - we change state later, depending on BaseState, ON and OFF
57
0
        thisOptionalContentGroup->setState(OptionalContentGroup::On);
58
0
        optionalContentGroups.emplace(ocgRef.getRef(), std::move(thisOptionalContentGroup));
59
0
    }
60
61
0
    Object defaultOcgConfig = ocgObject.dictLookup("D");
62
0
    if (!defaultOcgConfig.isDict()) {
63
0
        error(errSyntaxError, -1, "Expected the default config, but wasn't able to find it, or it isn't a Dictionary");
64
0
        ok = false;
65
0
        return;
66
0
    }
67
68
0
    Object baseState = defaultOcgConfig.dictLookup("BaseState");
69
0
    if (baseState.isName("OFF")) {
70
0
        for (auto &group : optionalContentGroups) {
71
0
            group.second->setState(OptionalContentGroup::Off);
72
0
        }
73
0
    }
74
75
0
    Object on = defaultOcgConfig.dictLookup("ON");
76
0
    if (on.isArray()) {
77
        // ON is an optional element
78
0
        Array *onArray = on.getArray();
79
0
        for (int i = 0; i < onArray->getLength(); ++i) {
80
0
            const Object &reference = onArray->getNF(i);
81
0
            if (!reference.isRef()) {
82
                // there can be null entries
83
0
                break;
84
0
            }
85
0
            OptionalContentGroup *group = findOcgByRef(reference.getRef());
86
0
            if (!group) {
87
0
                error(errSyntaxWarning, -1, "Couldn't find group for reference");
88
0
                break;
89
0
            }
90
0
            group->setState(OptionalContentGroup::On);
91
0
        }
92
0
    }
93
94
0
    Object off = defaultOcgConfig.dictLookup("OFF");
95
0
    if (off.isArray()) {
96
0
        Array *offArray = off.getArray();
97
        // OFF is an optional element
98
0
        for (int i = 0; i < offArray->getLength(); ++i) {
99
0
            const Object &reference = offArray->getNF(i);
100
0
            if (!reference.isRef()) {
101
                // there can be null entries
102
0
                break;
103
0
            }
104
0
            OptionalContentGroup *group = findOcgByRef(reference.getRef());
105
0
            if (!group) {
106
0
                error(errSyntaxWarning, -1, "Couldn't find group for reference to set OFF");
107
0
                break;
108
0
            }
109
0
            group->setState(OptionalContentGroup::Off);
110
0
        }
111
0
    }
112
113
0
    order = defaultOcgConfig.dictLookup("Order");
114
0
    rbgroups = defaultOcgConfig.dictLookup("RBGroups");
115
0
}
116
117
bool OCGs::hasOCGs() const
118
0
{
119
0
    return !(optionalContentGroups.empty());
120
0
}
121
122
OptionalContentGroup *OCGs::findOcgByRef(const Ref ref) const
123
0
{
124
0
    const auto ocg = optionalContentGroups.find(ref);
125
0
    return ocg != optionalContentGroups.end() ? ocg->second.get() : nullptr;
126
0
}
127
128
bool OCGs::optContentIsVisible(const Object *dictRef) const
129
0
{
130
0
    Dict *dict;
131
0
    bool result = true;
132
133
0
    if (dictRef->isNull()) {
134
0
        return result;
135
0
    }
136
137
0
    if (dictRef->isRef()) {
138
0
        OptionalContentGroup *oc = findOcgByRef(dictRef->getRef());
139
0
        if (oc) {
140
0
            return oc->getState() == OptionalContentGroup::On;
141
0
        }
142
0
    }
143
144
0
    Object dictObj = dictRef->fetch(m_xref);
145
0
    if (!dictObj.isDict()) {
146
0
        error(errSyntaxWarning, -1, "Unexpected oc reference target: {0:d}", dictObj.getType());
147
0
        return result;
148
0
    }
149
0
    dict = dictObj.getDict();
150
0
    Object dictType = dict->lookup("Type");
151
0
    if (dictType.isName("OCMD")) {
152
0
        Object ve = dict->lookup("VE");
153
0
        if (ve.isArray()) {
154
0
            result = evalOCVisibilityExpr(&ve, 0);
155
0
        } else {
156
0
            const Object &ocg = dict->lookupNF("OCGs");
157
0
            if (ocg.isArray()) {
158
0
                Object policy = dict->lookup("P");
159
0
                if (policy.isName("AllOn")) {
160
0
                    result = allOn(*ocg.getArray());
161
0
                } else if (policy.isName("AllOff")) {
162
0
                    result = allOff(*ocg.getArray());
163
0
                } else if (policy.isName("AnyOff")) {
164
0
                    result = anyOff(*ocg.getArray());
165
0
                } else if ((!policy.isName()) || (policy.isName("AnyOn"))) {
166
                    // this is the default
167
0
                    result = anyOn(*ocg.getArray());
168
0
                }
169
0
            } else if (ocg.isRef()) {
170
0
                OptionalContentGroup *oc = findOcgByRef(ocg.getRef());
171
0
                result = !oc || oc->getState() != OptionalContentGroup::Off;
172
0
            }
173
0
        }
174
0
    } else if (dictType.isName("OCG") && dictRef->isRef()) {
175
0
        OptionalContentGroup *oc = findOcgByRef(dictRef->getRef());
176
0
        if (oc && oc->getState() == OptionalContentGroup::Off) {
177
0
            result = false;
178
0
        }
179
0
    }
180
0
    return result;
181
0
}
182
183
bool OCGs::evalOCVisibilityExpr(const Object *expr, int recursion) const
184
0
{
185
0
    OptionalContentGroup *ocg;
186
0
    bool ret;
187
188
0
    if (recursion > visibilityExprRecursionLimit) {
189
0
        error(errSyntaxError, -1, "Loop detected in optional content visibility expression");
190
0
        return true;
191
0
    }
192
0
    if (expr->isRef()) {
193
0
        if ((ocg = findOcgByRef(expr->getRef()))) {
194
0
            return ocg->getState() == OptionalContentGroup::On;
195
0
        }
196
0
    }
197
0
    Object expr2 = expr->fetch(m_xref);
198
0
    if (!expr2.isArrayOfLengthAtLeast(1)) {
199
0
        error(errSyntaxError, -1, "Invalid optional content visibility expression");
200
0
        return true;
201
0
    }
202
0
    Array *expr2Array = expr2.getArray();
203
0
    Object op = expr2Array->get(0);
204
0
    if (op.isName("Not")) {
205
0
        if (expr2Array->getLength() == 2) {
206
0
            const Object &obj = expr2Array->getNF(1);
207
0
            ret = !evalOCVisibilityExpr(&obj, recursion + 1);
208
0
        } else {
209
0
            error(errSyntaxError, -1, "Invalid optional content visibility expression");
210
0
            ret = true;
211
0
        }
212
0
    } else if (op.isName("And")) {
213
0
        ret = true;
214
0
        for (int i = 1; i < expr2Array->getLength() && ret; ++i) {
215
0
            const Object &obj = expr2Array->getNF(i);
216
0
            ret = evalOCVisibilityExpr(&obj, recursion + 1);
217
0
        }
218
0
    } else if (op.isName("Or")) {
219
0
        ret = false;
220
0
        for (int i = 1; i < expr2Array->getLength() && !ret; ++i) {
221
0
            const Object &obj = expr2Array->getNF(i);
222
0
            ret = evalOCVisibilityExpr(&obj, recursion + 1);
223
0
        }
224
0
    } else {
225
0
        error(errSyntaxError, -1, "Invalid optional content visibility expression");
226
0
        ret = true;
227
0
    }
228
0
    return ret;
229
0
}
230
231
bool OCGs::allOn(const Array &ocgArray) const
232
0
{
233
0
    for (int i = 0; i < ocgArray.getLength(); ++i) {
234
0
        const Object &ocgItem = ocgArray.getNF(i);
235
0
        if (ocgItem.isRef()) {
236
0
            OptionalContentGroup *oc = findOcgByRef(ocgItem.getRef());
237
0
            if (oc && oc->getState() == OptionalContentGroup::Off) {
238
0
                return false;
239
0
            }
240
0
        }
241
0
    }
242
0
    return true;
243
0
}
244
245
bool OCGs::allOff(const Array &ocgArray) const
246
0
{
247
0
    for (int i = 0; i < ocgArray.getLength(); ++i) {
248
0
        const Object &ocgItem = ocgArray.getNF(i);
249
0
        if (ocgItem.isRef()) {
250
0
            OptionalContentGroup *oc = findOcgByRef(ocgItem.getRef());
251
0
            if (oc && oc->getState() == OptionalContentGroup::On) {
252
0
                return false;
253
0
            }
254
0
        }
255
0
    }
256
0
    return true;
257
0
}
258
259
bool OCGs::anyOn(const Array &ocgArray) const
260
0
{
261
0
    for (int i = 0; i < ocgArray.getLength(); ++i) {
262
0
        const Object &ocgItem = ocgArray.getNF(i);
263
0
        if (ocgItem.isRef()) {
264
0
            OptionalContentGroup *oc = findOcgByRef(ocgItem.getRef());
265
0
            if (oc && oc->getState() == OptionalContentGroup::On) {
266
0
                return true;
267
0
            }
268
0
        }
269
0
    }
270
0
    return false;
271
0
}
272
273
bool OCGs::anyOff(const Array &ocgArray) const
274
0
{
275
0
    for (int i = 0; i < ocgArray.getLength(); ++i) {
276
0
        const Object &ocgItem = ocgArray.getNF(i);
277
0
        if (ocgItem.isRef()) {
278
0
            OptionalContentGroup *oc = findOcgByRef(ocgItem.getRef());
279
0
            if (oc && oc->getState() == OptionalContentGroup::Off) {
280
0
                return true;
281
0
            }
282
0
        }
283
0
    }
284
0
    return false;
285
0
}
286
287
//------------------------------------------------------------------------
288
289
0
OptionalContentGroup::OptionalContentGroup(Dict *ocgDict) : m_name(nullptr)
290
0
{
291
0
    Object ocgName = ocgDict->lookup("Name");
292
0
    if (!ocgName.isString()) {
293
0
        error(errSyntaxWarning, -1, "Expected the name of the OCG, but wasn't able to find it, or it isn't a String");
294
0
    } else {
295
0
        m_name = ocgName.takeString();
296
0
    }
297
298
0
    viewState = printState = ocUsageUnset;
299
0
    Object obj1 = ocgDict->lookup("Usage");
300
0
    if (obj1.isDict()) {
301
0
        Object obj2 = obj1.dictLookup("View");
302
0
        if (obj2.isDict()) {
303
0
            Object obj3 = obj2.dictLookup("ViewState");
304
0
            if (obj3.isName()) {
305
0
                if (obj3.isName("ON")) {
306
0
                    viewState = ocUsageOn;
307
0
                } else {
308
0
                    viewState = ocUsageOff;
309
0
                }
310
0
            }
311
0
        }
312
0
        obj2 = obj1.dictLookup("Print");
313
0
        if (obj2.isDict()) {
314
0
            Object obj3 = obj2.dictLookup("PrintState");
315
0
            if (obj3.isName()) {
316
0
                if (obj3.isName("ON")) {
317
0
                    printState = ocUsageOn;
318
0
                } else {
319
0
                    printState = ocUsageOff;
320
0
                }
321
0
            }
322
0
        }
323
0
    }
324
0
}
325
326
const GooString *OptionalContentGroup::getName() const
327
0
{
328
0
    return m_name.get();
329
0
}
330
331
void OptionalContentGroup::setRef(const Ref ref)
332
0
{
333
0
    m_ref = ref;
334
0
}
335
336
Ref OptionalContentGroup::getRef() const
337
0
{
338
0
    return m_ref;
339
0
}
340
341
0
OptionalContentGroup::~OptionalContentGroup() = default;