1/*
2 SPDX-FileCopyrightText: 2025 Stefan BrĂ¼ns <stefan.bruens@rwth-aachen.de>
3 SPDX-License-Identifier: LGPL-2.1-or-later
4*/
5
6#include "../lib/decompressor.h"
7
8#include <QBuffer>
9#include <QTest>
10#include <QVector>
11#include <QtEndian>
12#include <array>
13
14using namespace Mobipocket;
15
16class DecompressorTest : public QObject
17{
18 Q_OBJECT
19private Q_SLOTS:
20 void testNoop();
21 void testNoop_data();
22 void testRLE();
23 void testRLE_data();
24 void testHuffInit();
25 void testHuffDecompress();
26 void testFuzzHuff();
27 void benchmarkHuffDecompress();
28};
29
30namespace {
31 QVector<QByteArray> createHuffIdentityDict()
32 {
33 // Create a Huffman dictionary which maps each input byte to itself
34 static std::array<quint8, 256 * 4> hdict = []() {
35 std::array<quint8, 256 * 4> d;
36 for (size_t i = 0; i < d.size(); i += 4) {
37 // 1. Codelen is 8 bits
38 // 2. Only use the first tree dictionary, set the termination flag
39 d[i] = 8 | 0x80;
40 d[i + 1] = i / 2;
41 d[i + 2] = i / 512;
42 }
43 return d;
44 }();
45
46 QByteArray huff("HUFF", 4);
47 huff.resize(size: 24);
48 qToBigEndian<quint32>(src: huff.size(), dest: huff.data() + 16);
49#if QT_VERSION > QT_VERSION_CHECK(6, 0, 0)
50 huff.append(a: QByteArrayView(hdict));
51#else
52 huff.append(QByteArray::fromRawData(reinterpret_cast<char*>(hdict.data()), hdict.size()));
53#endif
54 qToBigEndian<quint32>(src: huff.size(), dest: huff.data() + 20);
55 huff.append(n: 64 * 4, ch: '\0');
56
57 static std::array<quint8, 256 * (2 + 3)> entries = []() {
58 std::array<quint8, 256 * (2 + 3)> d;
59 for (size_t i = 0; i < 256; i++) {
60 quint16 off = 512 + 3 * i;
61 qToBigEndian<quint16>(src: off, dest: &d[2 * i]);
62 qToBigEndian<quint16>(src: 0x8001, dest: &d[off]); // len==1 | termination flag
63 d[off + 2] = i;
64 }
65 return d;
66 }();
67
68 QByteArray cdic("CDIC\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", 16);
69 qToBigEndian<quint32>(src: 32, dest: cdic.data() + 12);
70#if QT_VERSION > QT_VERSION_CHECK(6, 0, 0)
71 cdic.append(a: QByteArrayView(entries));
72#else
73 cdic.append(QByteArray::fromRawData(reinterpret_cast<char*>(entries.data()), entries.size()));
74#endif
75
76 return {huff, cdic};
77 }
78}
79
80void DecompressorTest::testNoop()
81{
82 QFETCH(QByteArray, data);
83
84 auto decompressor = Decompressor::create(type: 1, auxData: {});
85
86 auto r = decompressor->decompress(data);
87 // NOOP -> input and output are identical
88 QCOMPARE(r, data);
89}
90
91void DecompressorTest::testNoop_data()
92{
93 QTest::addColumn<QByteArray>(name: "data");
94
95 QTest::addRow(format: "empty") << QByteArray();
96 QTest::addRow(format: "0x00 * 10") << QByteArray(10, '\x00');
97 QTest::addRow(format: "0xaa * 10") << QByteArray(10, '\xaa');
98}
99
100void DecompressorTest::testRLE()
101{
102 QFETCH(QByteArray, data);
103 QFETCH(QByteArray, expected);
104
105 auto decompressor = Decompressor::create(type: 2, auxData: {});
106
107 auto r = decompressor->decompress(data);
108 QCOMPARE(r, expected);
109}
110
111void DecompressorTest::testRLE_data()
112{
113 QTest::addColumn<QByteArray>(name: "data");
114 QTest::addColumn<QByteArray>(name: "expected");
115
116 QTest::addRow(format: "empty") << QByteArray() << QByteArray();
117 // Token '0x00' is passed verbatim
118 QTest::addRow(format: "0x00 * 10") << QByteArray(10, '\x00') << QByteArray(10, '\x00');
119
120 // Tokens in the range 0x01..0x08 denotes the length of raw copied data
121 QTest::addRow(format: "raw 0x01...") << QByteArray("\x01\xff", 2) << QByteArray("\xff", 1);
122 QTest::addRow(format: "raw .0x01...") << QByteArray("d\x01\xc0kj", 5) << QByteArray("d\xc0kj", 4);
123 QTest::addRow(format: "raw .0x02...") << QByteArray("d\x02\xc0kj", 5) << QByteArray("d\xc0kj", 4);
124 QTest::addRow(format: "raw .0x03...") << QByteArray("d\x03\xc0kj", 5) << QByteArray("d\xc0kj", 4);
125 // Short data
126 QTest::addRow(format: "short 0x03...") << QByteArray("d\x03\xc0k", 4) << QByteArray("d", 1);
127
128 // Tokens in the range 0x09..0x7f are passed verbatim
129 QTest::addRow(format: "0x20 * 20") << QByteArray(20, '\x20') << QByteArray(20, '\x20');
130 QTest::addRow(format: "0x7f * 20") << QByteArray(20, '\x7f') << QByteArray(20, '\x7f');
131
132 // Tokens in the range 0xc0..0xff are expanded to " \x40".." \x7f"
133 QTest::addRow(format: "0xc0 * 64") << QByteArray(64, '\xc0') << QByteArray(" \x40", 2).repeated(times: 64);
134 QTest::addRow(format: "0xf0 * 64") << QByteArray(64, '\xf0') << QByteArray(" \x70", 2).repeated(times: 64);
135
136 QTest::addRow(format: "repeat") << QByteArray("\x32\x80\x0a", 3) << QByteArray(6, '2');
137 QTest::addRow(format: "repeat 2") << QByteArray("\x31\x65\x80\x13", 4) << QByteArray("1e").repeated(times: 4);
138}
139
140void DecompressorTest::testHuffInit()
141{
142 {
143 auto decompressor = Decompressor::create(type: 'H', auxData: {});
144 QVERIFY(!decompressor->isValid());
145 }
146 {
147 auto decompressor = Decompressor::create(type: 'H', auxData: QVector<QByteArray>(2));
148 QVERIFY(!decompressor->isValid());
149 }
150 {
151 QByteArray HDic(512, '\0');
152 QByteArray CDic(512, '\0');
153
154 auto decompressor = Decompressor::create(type: 'H', auxData: {HDic, CDic});
155 QVERIFY(!decompressor->isValid());
156 }
157 {
158 QByteArray HDic("HUFF", 4);
159 QByteArray CDic("CDIC", 4);
160 QByteArray fill(60, '\0');
161
162 auto decompressor = Decompressor::create(type: 'H', auxData: {HDic + fill, CDic + fill});
163 QVERIFY(!decompressor->isValid());
164 }
165 {
166 auto decompressor = Decompressor::create(type: 'H', auxData: createHuffIdentityDict());
167 QVERIFY(decompressor->isValid());
168 }
169}
170
171void DecompressorTest::testHuffDecompress()
172{
173 auto decompressor = Decompressor::create(type: 'H', auxData: createHuffIdentityDict());
174 QVERIFY(decompressor->isValid());
175
176 {
177 auto r = decompressor->decompress(data: QByteArray("\0", 1));
178 QCOMPARE(r, QByteArray("\0", 1));
179 }
180 {
181 auto r = decompressor->decompress(data: QByteArray("\1\xcc", 2));
182 QCOMPARE(r, QByteArray("\1\xcc", 2));
183 }
184 {
185 QByteArray d(256, '\0');
186 for (int i = 0; i < d.size(); i++) {
187 d[i] = i;
188 }
189 auto r = decompressor->decompress(data: d);
190 QCOMPARE(r, d);
191 }
192}
193
194void DecompressorTest::benchmarkHuffDecompress()
195{
196 auto decompressor = Decompressor::create(type: 'H', auxData: createHuffIdentityDict());
197 QVERIFY(decompressor->isValid());
198
199 QByteArray data(1024, '\x01');
200
201 QBENCHMARK {
202 auto r = decompressor->decompress(data);
203 QCOMPARE(r, data);
204 }
205}
206
207void DecompressorTest::testFuzzHuff()
208{
209 auto verify = [](const auto &decompressor) {
210 QByteArray d(256, '\0');
211 for (int i = 0; i < d.size(); i++) {
212 d[i] = i;
213 }
214 // The output does not matter and is likely
215 // just garbage, but it should not crash
216 auto r = decompressor->decompress(d);
217 };
218
219 auto dict = createHuffIdentityDict();
220
221 for (auto i = dict.at(i: 0).size() - 1; i >= 0; i--) {
222 unsigned char originalValue = dict.at(i: 0).at(i);
223
224 {
225 dict[0][i] = originalValue ^ 0xff;
226 auto decompressor = Decompressor::create(type: 'H', auxData: dict);
227 verify(decompressor);
228 }
229
230 if ((originalValue == 0) || (originalValue == 0xff)) {
231 dict[0][i] = originalValue;
232 continue;
233 }
234
235 {
236 dict[0][i] = 0;
237 auto decompressor = Decompressor::create(type: 'H', auxData: dict);
238 verify(decompressor);
239 }
240
241 {
242 dict[0][i] = static_cast<unsigned char>(0xff);
243 auto decompressor = Decompressor::create(type: 'H', auxData: dict);
244 verify(decompressor);
245 }
246
247 dict[0][i] = originalValue;
248 }
249}
250
251QTEST_GUILESS_MAIN(DecompressorTest)
252
253#include "decompressortest.moc"
254

source code of kdegraphics-mobipocket/autotests/decompressortest.cpp