Skip to content

Commit ce45fe3

Browse files
committed
Add support for serving Qt resources in their internal compressed form, if the Accept-Encoding headers allows.
1 parent b23dcc1 commit ce45fe3

14 files changed

Lines changed: 737 additions & 14 deletions

REUSE.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ SPDX-PackageDownloadLocation = "https://www.github.com/KDAB/KDSoap"
55

66
#misc source code
77
[[annotations]]
8-
path = ["unittests/**/**.xsd", "unittests/**/**.wsdl", "unittests/**/**.XSD", "unittests/**/**.xml", "testtools/**.qrc", "examples/**/**.qrc", "examples/**/**.wsdl", "**.html", "**.css", "docs/manual/**.pdf", "**.bat", "**.pem"]
8+
path = ["unittests/**/**.xsd", "unittests/**/**.wsdl", "unittests/**/**.XSD", "unittests/**/**.xml", "unittests/**/**.qrc", "testtools/**.qrc", "examples/**/**.qrc", "examples/**/**.wsdl", "**.html", "**.css", "docs/manual/**.pdf", "**.bat", "**.pem"]
99
precedence = "aggregate"
1010
SPDX-FileCopyrightText = "Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>"
1111
SPDX-License-Identifier = "MIT"
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/****************************************************************************
2+
**
3+
** This file is part of the KD Soap project.
4+
**
5+
** SPDX-FileCopyrightText: 2025 Jonathan Brady <jtjbrady@users.noreply.github.com>
6+
**
7+
** SPDX-License-Identifier: MIT
8+
**
9+
****************************************************************************/
10+
11+
#ifndef BYTEARRAYVIEWSPLITTER_H
12+
#define BYTEARRAYVIEWSPLITTER_H
13+
14+
#include <QByteArrayView>
15+
#include <QDebug>
16+
#include <iterator>
17+
#include <type_traits>
18+
#if defined(__cpp_lib_ranges) && __cpp_lib_ranges >= 201911L
19+
#include <ranges>
20+
#endif
21+
22+
// A zero-copy byte-splitting view over a QByteArrayView.
23+
// Splits by a character or QByteArrayView delimiter, with optional skipping of empty parts.
24+
template<typename Delim>
25+
class ByteArrayViewSplitter
26+
#if defined(__cpp_lib_ranges) && __cpp_lib_ranges >= 201911L
27+
: public std::ranges::view_interface<QByteArraySplitter<Delim>>
28+
#endif
29+
{
30+
using DelimType = std::decay_t<Delim>;
31+
32+
// Enforce supported delimiter types: byte values or QByteArrayView
33+
static_assert(
34+
std::is_same_v<DelimType, char> || std::is_same_v<DelimType, signed char> || std::is_same_v<DelimType, unsigned char> || std::is_same_v<DelimType, std::byte> || std::is_same_v<DelimType, QByteArrayView>,
35+
"Delimiter must be a valid QByteArrayView byte type or QByteArrayView itself");
36+
37+
public:
38+
ByteArrayViewSplitter(QByteArrayView origin, Delim delim, Qt::SplitBehavior skipEmpty = Qt::SplitBehaviorFlags::KeepEmptyParts)
39+
: m_origin(origin)
40+
, m_delim(delim)
41+
, m_skipEmpty(skipEmpty == Qt::SplitBehaviorFlags::SkipEmptyParts)
42+
{
43+
}
44+
45+
class iterator
46+
{
47+
// Returns 1 for char-like delimiters, or size() for QByteArrayView delimiters
48+
static constexpr qsizetype computeDelimSize(const DelimType &delim)
49+
{
50+
if constexpr (std::is_same_v<DelimType, QByteArrayView>)
51+
return delim.size();
52+
else
53+
return 1;
54+
}
55+
56+
public:
57+
using value_type = QByteArrayView;
58+
using difference_type = std::ptrdiff_t;
59+
using iterator_category = std::forward_iterator_tag;
60+
61+
iterator() = default;
62+
63+
iterator(QByteArrayView origin, Delim delim, qsizetype offset, bool skipEmpty)
64+
: m_origin(origin)
65+
, m_delim(delim)
66+
, m_delimSize(computeDelimSize(delim))
67+
, m_offset(offset)
68+
, m_skipEmpty(skipEmpty)
69+
{
70+
if (m_offset < 0) {
71+
// Marks end iterator
72+
m_offset = m_nextOffset = -1;
73+
} else {
74+
// Start at offset, advance once to initialize m_nextOffset
75+
m_nextOffset = m_offset;
76+
do {
77+
advance();
78+
} while (m_skipEmpty && m_offset >= 0 && m_offset + m_delimSize == m_nextOffset); // Skip empty parts
79+
}
80+
}
81+
82+
// Returns the current part
83+
QByteArrayView operator*() const
84+
{
85+
if (m_offset < 0)
86+
return {};
87+
88+
const char *start = m_origin.data() + m_offset;
89+
const char *end = m_origin.data() + m_nextOffset - m_delimSize;
90+
91+
return QByteArrayView(start, end - start);
92+
}
93+
94+
// Advance to the next part
95+
iterator &operator++()
96+
{
97+
do {
98+
advance();
99+
} while (m_skipEmpty && m_offset >= 0 && m_offset + m_delimSize == m_nextOffset); // Skip empty parts
100+
101+
return *this;
102+
}
103+
104+
#if defined(__cpp_lib_ranges) && __cpp_lib_ranges >= 201911L
105+
// C++20 sentinel-based end comparison
106+
bool operator==(std::default_sentinel_t) const
107+
{
108+
return m_offset < 0;
109+
}
110+
#endif
111+
112+
// Equality comparison for pre-C++20 use
113+
bool operator==(const iterator &other) const
114+
{
115+
return m_offset == other.m_offset && m_origin.data() == other.m_origin.data() && m_origin.size() == other.m_origin.size();
116+
}
117+
118+
bool operator!=(const iterator &other) const
119+
{
120+
return !(*this == other);
121+
}
122+
123+
private:
124+
// Advances m_offset and m_nextOffset to the next segment
125+
void advance()
126+
{
127+
m_offset = m_nextOffset;
128+
129+
if (m_offset > m_origin.size()) {
130+
m_offset = -1; // Marks end
131+
return;
132+
}
133+
134+
// Find next delimiter
135+
const qsizetype delimPos = m_origin.indexOf(m_delim, m_offset);
136+
if (delimPos != -1) {
137+
m_nextOffset = delimPos + m_delimSize;
138+
} else {
139+
// No more delimiters: consume rest of input
140+
m_nextOffset = m_origin.size() + m_delimSize;
141+
}
142+
}
143+
144+
const QByteArrayView m_origin;
145+
const DelimType m_delim;
146+
const qsizetype m_delimSize;
147+
qsizetype m_offset = 0; // Start of current part
148+
qsizetype m_nextOffset = 0; // End of current part (includes delimiter)
149+
const bool m_skipEmpty = false;
150+
};
151+
152+
iterator begin() const
153+
{
154+
return iterator {m_origin, m_delim, qsizetype(0), m_skipEmpty};
155+
}
156+
157+
#if defined(__cpp_lib_ranges) && __cpp_lib_ranges >= 201911L
158+
// C++20 sentinel-based end
159+
std::default_sentinel_t end() const noexcept
160+
{
161+
return {};
162+
}
163+
#else
164+
// Classic iterator-based end
165+
iterator end() const noexcept
166+
{
167+
return iterator {m_origin, m_delim, qsizetype(-1), m_skipEmpty};
168+
}
169+
#endif
170+
171+
private:
172+
const QByteArrayView m_origin;
173+
const DelimType m_delim;
174+
const bool m_skipEmpty;
175+
};
176+
177+
#endif

src/KDSoapServer/KDSoapServerObjectInterface.cpp

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,15 @@
1010
#include "KDSoapServerObjectInterface.h"
1111
#include "KDSoapClient/KDSoapValue.h"
1212
#include "KDSoapServerSocket_p.h"
13+
#include <QBuffer>
1314
#include <QDebug>
15+
#include <QFileInfo>
16+
#include <QMetaEnum>
1417
#include <QPointer>
18+
#include <QResource>
19+
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
20+
#include "ByteArrayViewSplitter.h"
21+
#endif
1522

1623
class KDSoapServerObjectInterface::Private
1724
{
@@ -33,6 +40,7 @@ class KDSoapServerObjectInterface::Private
3340
KDSoap::SoapVersion m_requestVersion = KDSoap::SoapVersion::SOAP1_1;
3441
// QPointer in case the client disconnects during a delayed response
3542
QPointer<KDSoapServerSocket> m_serverSocket;
43+
QMap<QByteArray, QByteArray> m_httpHeaders;
3644
};
3745

3846
KDSoapServerObjectInterface::HttpResponseHeaderItem::HttpResponseHeaderItem(const QByteArray &name, const QByteArray &value)
@@ -183,6 +191,7 @@ KDSoapDelayedResponseHandle KDSoapServerObjectInterface::prepareDelayedResponse(
183191
void KDSoapServerObjectInterface::setServerSocket(KDSoapServerSocket *serverSocket)
184192
{
185193
d->m_serverSocket = serverSocket;
194+
d->m_httpHeaders = d->m_serverSocket->m_httpHeaders;
186195
}
187196

188197
void KDSoapServerObjectInterface::sendDelayedResponse(const KDSoapDelayedResponseHandle &responseHandle, const KDSoapMessage &response)
@@ -193,6 +202,161 @@ void KDSoapServerObjectInterface::sendDelayedResponse(const KDSoapDelayedRespons
193202
}
194203
}
195204

205+
namespace {
206+
// Parses an Accept-Encoding header into a bitmask of EncodingFormat values.
207+
//
208+
// See RFC 9110 §12.5.3: Accept-Encoding
209+
// Recognizes q-values (q=0 means "not acceptable") and wildcard "*" entries.
210+
//
211+
// Accept-Encoding: gzip, deflate, br, identity;q=0
212+
// Accept-Encoding: *, identity;q=0 → means "anything except identity"
213+
// Accept-Encoding: *;q=0, gzip → means "anything except gzip"
214+
KDSoapServerSocket::EncodingFormats parseAcceptEncoding(const QByteArray &headerValue)
215+
{
216+
KDSoapServerSocket::EncodingFormats formats; // Accepted formats
217+
KDSoapServerSocket::EncodingFormats rejectedFormats; // Explicitly rejected formats (q=0)
218+
219+
const QMetaEnum metaEnum = QMetaEnum::fromType<KDSoapServerSocket::EncodingFormats>();
220+
221+
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
222+
using PartType = QByteArrayView;
223+
const auto parts = ByteArrayViewSplitter(headerValue, ',', Qt::SkipEmptyParts);
224+
#else
225+
using PartType = QByteArray;
226+
const auto parts = headerValue.split(',');
227+
#endif
228+
229+
for (const auto &rawPart : parts) {
230+
const auto part = rawPart.trimmed();
231+
if (part.isEmpty())
232+
continue;
233+
234+
// Per RFC 9110 §12.5.3, we parse each `token[;q=value]`
235+
const int semiPos = part.indexOf(';');
236+
const auto encoding = semiPos >= 0 ? part.left(semiPos).trimmed() : part;
237+
const auto params = semiPos >= 0 ? part.mid(semiPos + 1).trimmed() : PartType();
238+
239+
// Parse q= parameter (if present) looking for q=0 (rejection)
240+
bool rejected = false;
241+
if (params.startsWith("q=")) {
242+
bool ok = false;
243+
const float qVal = params.mid(2).trimmed().toFloat(&ok);
244+
if (ok && qVal == 0.0f) {
245+
rejected = true;
246+
}
247+
}
248+
249+
// Handle wildcard "*" — add all known formats not explicitly rejected
250+
if (encoding == "*") {
251+
if (!rejected) {
252+
// Accept all known formats (per our enum) except already rejected ones
253+
for (int i = 0; i < metaEnum.keyCount(); ++i) {
254+
const auto val = static_cast<KDSoapServerSocket::EncodingFormat>(metaEnum.value(i));
255+
if (!rejectedFormats.testFlag(val))
256+
formats |= val;
257+
}
258+
} else if (!formats.testFlag(KDSoapServerSocket::EncodingFormat::identity)) {
259+
// Reject all formats not specifically mentioned, including identity
260+
rejectedFormats |= KDSoapServerSocket::EncodingFormat::identity;
261+
qInfo() << __LINE__ << formats << rejectedFormats;
262+
}
263+
continue;
264+
}
265+
266+
// Normalize encoding to lowercase and look up enum value
267+
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
268+
const int value = metaEnum.keyToValue(QByteArray(encoding.constData(), encoding.size()).toLower().constData());
269+
#else
270+
const int value = metaEnum.keyToValue(encoding.toLower().constData());
271+
#endif
272+
if (value >= 0) {
273+
const auto format = static_cast<KDSoapServerSocket::EncodingFormats>(value);
274+
275+
if (rejected) {
276+
// Remove from accepted in case it was added earlier (e.g. by *) and add to rejected
277+
formats &= ~format;
278+
rejectedFormats |= format;
279+
} else {
280+
// Accept format and remove from rejected
281+
formats |= format;
282+
rejectedFormats &= ~format;
283+
}
284+
}
285+
// Unknown encodings are ignored per RFC 9110 §12.5.3
286+
}
287+
288+
// Ensure identity is accepted unless explicitly rejected
289+
if (!rejectedFormats.testFlag(KDSoapServerSocket::EncodingFormat::identity)) {
290+
formats |= KDSoapServerSocket::EncodingFormat::identity;
291+
}
292+
293+
return formats;
294+
}
295+
296+
// Convert QResource::Compression enum to our HTTP encoding enum
297+
KDSoapServerSocket::EncodingFormat compressionToEncodingFormat(QResource::Compression c)
298+
{
299+
switch (c) {
300+
case QResource::NoCompression:
301+
return KDSoapServerSocket::EncodingFormat::identity;
302+
case QResource::ZlibCompression:
303+
// Qt uses zlib internally, HTTP calls this "deflate" (RFC 7231 §4.2.2)
304+
return KDSoapServerSocket::EncodingFormat::deflate;
305+
case QResource::ZstdCompression:
306+
// zstd
307+
return KDSoapServerSocket::EncodingFormat::zstd;
308+
}
309+
return KDSoapServerSocket::EncodingFormat::identity;
310+
}
311+
}
312+
313+
std::pair<QIODevice *, QByteArray> KDSoapServerObjectInterface::fileForEncoding(const QString &logicalPath)
314+
{
315+
QFileInfo fileInfo(logicalPath);
316+
if (!fileInfo.exists()) {
317+
qWarning() << "File does not exist:" << logicalPath;
318+
return {nullptr, QByteArrayLiteral("HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n")};
319+
}
320+
321+
const QString resolvedPath = fileInfo.absoluteFilePath();
322+
const auto acceptedEncodings = parseAcceptEncoding(d->m_httpHeaders.value("accept-encoding"));
323+
324+
#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)
325+
// Attempt to interpret as a Qt resource
326+
if (QResource res(resolvedPath); res.isValid()) {
327+
KDSoapServerSocket::EncodingFormat resourceCompression = compressionToEncodingFormat(res.compressionAlgorithm());
328+
329+
// If resource is compressed and the format is accepted
330+
if (resourceCompression != KDSoapServerSocket::EncodingFormat::identity && acceptedEncodings.testFlag(resourceCompression)) {
331+
QBuffer *buffer = new QBuffer;
332+
// Per https://doc.qt.io/archives/qt-5.13/qresource.html and
333+
// https://doc.qt.io/archives/qt-5.13/qbytearray.html#qUncompress
334+
// the resource has a 4 byte header which contains the big endian uncompressed size.
335+
buffer->setData(reinterpret_cast<const char *>(res.data() + 4), res.size() - 4);
336+
337+
// Return compressed content with Content-Encoding
338+
const auto metaEnum = QMetaEnum::fromType<KDSoapServerSocket::EncodingFormats>();
339+
return {buffer, QByteArray::fromRawData(metaEnum.valueToKey(resourceCompression), ::qstrlen(metaEnum.valueToKey(resourceCompression)))};
340+
}
341+
}
342+
#endif
343+
344+
// At this point:
345+
// - Either it's not a Qt resource, or
346+
// - It's a resource but compression was not accepted
347+
348+
// RFC 9110 §8.4.4: "If the identity encoding is not acceptable, the origin server SHOULD send a 406 (Not Acceptable) response."
349+
if (!acceptedEncodings.testFlag(KDSoapServerSocket::EncodingFormat::identity)) {
350+
qWarning() << "Uncompressed file rejected by Accept-Encoding:" << resolvedPath;
351+
return {nullptr, QByteArrayLiteral("HTTP/1.1 406 Not Acceptable\r\nContent-Length: 0\r\n\r\n")};
352+
}
353+
354+
// Fall back to serving uncompressed version via QFile
355+
QFile *file = new QFile(resolvedPath);
356+
357+
return {file, QByteArray()};
358+
}
359+
196360
void KDSoapServerObjectInterface::writeHTTP(const QByteArray &httpReply)
197361
{
198362
const qint64 written = d->m_serverSocket->write(httpReply);
@@ -210,6 +374,7 @@ void KDSoapServerObjectInterface::copyFrom(KDSoapServerObjectInterface *other)
210374
d->m_requestHeaders = other->d->m_requestHeaders;
211375
d->m_soapAction = other->d->m_soapAction;
212376
d->m_serverSocket = other->d->m_serverSocket;
377+
d->m_httpHeaders = other->d->m_httpHeaders;
213378
d->m_requestVersion = other->d->m_requestVersion;
214379
}
215380

0 commit comments

Comments
 (0)