diff --git a/include/QHttpEngine/QHttpRange b/include/QHttpEngine/QHttpRange new file mode 100644 index 0000000..d5f9920 --- /dev/null +++ b/include/QHttpEngine/QHttpRange @@ -0,0 +1 @@ +#include "qhttprange.h" diff --git a/include/QHttpEngine/qhttprange.h b/include/QHttpEngine/qhttprange.h new file mode 100644 index 0000000..703faa6 --- /dev/null +++ b/include/QHttpEngine/qhttprange.h @@ -0,0 +1,264 @@ +/* + * Copyright (c) 2016 Aleksei Ermakov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +#ifndef QHTTPENGINE_QHTTPRANGE_H +#define QHTTPENGINE_QHTTPRANGE_H + +#include + +#include "qhttpengine.h" + +class QHTTPENGINE_EXPORT QHttpRangePrivate; + +/** + * @brief HTTP range representation + * @headerfile qhttpparser.h QHttpEngine/QHttpParser + * + * This class provides a representation of HTTP range, described in RFC 7233 + * and used when partial content is requested by client. When object is + * created, optional dataSize can be specified, so that relative ranges can + * be represented as absolute. + * + * Example: + * @code + * QHttpRange range(10, -1, 90); + * range.from(); // 10 + * range.to(); // 89 + * range.length(); // 80 + * + * range = QHttpRange("-500", 1000); + * range.from(); // 500 + * range.to(); // 999 + * range.length(); // 500 + * + * range = QHttpRange(0, -1); + * range.from(); // 0 + * range.to(); // -1 + * range.length(); // -1 + * + * range = QHttpRange(range, 100); + * range.from(); // 0 + * range.to(); // 99 + * range.length(); // 100 + * @endcode + * + */ +class QHTTPENGINE_EXPORT QHttpRange +{ +public: + + /** + * @brief Constructs QHttpRange by parsing range. + * + * Parses string representation range and constructs new QHttpRange. + * For raw header "Range: bytes=0-100" only "0-100" should be passed to + * constructor. dataSize may be supplied so that relative ranges could be + * represented as absolute values. + */ + QHttpRange(const QString &range, qint64 dataSize = -1); + + /** + * @brief Constructs QHttpRange, using from and to values. + * + * Initialises a new QHttpRange with from and to values. dataSize may be + * supplied so that relative ranges could be represented as + * absolute values. + */ + QHttpRange(qint64 from, qint64 to, qint64 dataSize = -1); + + /** + * @brief Constructs QHttpRange from other QHttpRange and dataSize. + * + * Initialises a new QHttpRange with from and to values of other + * QHttpRequest. Supplied dataSize is used instead of other dataSize. + */ + QHttpRange(const QHttpRange &other, qint64 dataSize); + + /** + * @brief Default QHttpRange constructor. + * + * Default empty QHttpRange is considered invalid. + */ + QHttpRange(); + ~QHttpRange(); + + QHttpRange& operator=(const QHttpRange &other); + + /** + * @brief Returns starting position of range. + * + * If range is set as 'last N bytes' and dataSize is not set, returns -N. + * + * Example: + * @code + * QHttpRange range("-500"); + * range.from(); // -500 + * range.to(); // -1 + * range.length(); // 500 + * + * range = QHttpRange(range, 800); + * range.from(); // 300 + * range.to(); // 799 + * range.length(); // 500 + * + * range = QHttpRange("10-"); + * range.from(); // 10 + * range.to(); // -1 + * range.length(); // -1 + * + * range = QHttpRange(range, 100); + * range.from(); // 10 + * range.to(); // 99 + * range.length(); // 90 + * @endcode + * + */ + qint64 from() const; + + /** + * @brief Returns ending position of range. + * + * If range is set as 'last N bytes' and dataSize is not set, returns -1. + * If ending position is not set, and dataSize is not set, returns -1. + * + * Example: + * @code + * QHttpRange range("-500"); + * range.from(); // -500 + * range.to(); // -1 + * range.length(); // 500 + * + * range = QHttpRange(range, 800); + * range.from(); // 300 + * range.to(); // 799 + * range.length(); // 500 + * + * range = QHttpRange("10-"); + * range.from(); // 10 + * range.to(); // -1 + * range.length(); // -1 + * + * range = QHttpRange(range, 100); + * range.from(); // 10 + * range.to(); // 99 + * range.length(); // 90 + * @endcode + * + */ + qint64 to() const; + + /** + * @brief Returns length of range. + * + * If ending position is not set, and dataSize is not set, and range is + * not set as 'last N bytes', returns -1. If range is invalid, returns -1. + * + * Example: + * @code + * QHttpRange range("-500"); + * range.from(); // -500 + * range.to(); // -1 + * range.length(); // 500 + * + * range = QHttpRange(range, 800); + * range.from(); // 300 + * range.to(); // 799 + * range.length(); // 500 + * + * range = QHttpRange("10-"); + * range.from(); // 10 + * range.to(); // -1 + * range.length(); // -1 + * + * range = QHttpRange(range, 100); + * range.from(); // 10 + * range.to(); // 99 + * range.length(); // 90 + * @endcode + * + */ + qint64 length() const; + + /** + * @brief Returns dataSize of range. + * + * If dataSize is not set, returns -1. + */ + qint64 dataSize() const; + + /** + * @brief Checks if range is valid + * + * Range is considered invalid if it is out of bounds, that is when this + * inequality is false - (from <= to < dataSize). + * When QHttpRange(const QString&) fails to parse range string, resulting + * range is also considered invalid. + * + * Example: + * @code + * QHttpRange range(1, 0, -1); + * range.isValid(); // false + * + * range = QHttpRange(512, 1024); + * range.isValid(); // true + * + * range = QHttpRange("-"); + * range.isValid(); // false + * + * range = QHttpRange("abccbf"); + * range.isValid(); // false + * + * range = QHttpRange(0, 512, 128); + * range.isValid(); // false + * + * range = QHttpRange(128, 64, 512); + * range.isValid(); // false + * @endcode + */ + bool isValid() const; + + /** + * @brief Returns representation suitable for Content-Range header. + * + * Example: + * @code + * QHttpRange range(0, 100, 1000); + * range.contentRange(); // "0-100/1000" + * + * // When resource size is unknown + * range = QHttpRange(512, 1024); + * range.contentRange(); // "512-1024/*" + * + * // if range request was bad, return resource size + * range = QHttpRange(1, 0, 1200); + * range.contentRange(); // "*\/1200" + * @endcode + */ + QString contentRange() const; + +private: + + QHttpRangePrivate *const d; + friend class QHttpRangePrivate; +}; + +#endif // QHTTPENGINE_QHTTPRANGE_H diff --git a/include/QHttpEngine/qhttpsocket.h b/include/QHttpEngine/qhttpsocket.h index 8bc214b..9e5e8d2 100644 --- a/include/QHttpEngine/qhttpsocket.h +++ b/include/QHttpEngine/qhttpsocket.h @@ -138,6 +138,8 @@ public: enum { /// Request was successful OK = 200, + /// Range request was successful + PartialContent = 206, /// Resource has moved permanently MovedPermanently = 301, /// Resource is available at an alternate URI diff --git a/include/QHttpEngine/qiodevicecopier.h b/include/QHttpEngine/qiodevicecopier.h index ad8128c..9395c65 100644 --- a/include/QHttpEngine/qiodevicecopier.h +++ b/include/QHttpEngine/qiodevicecopier.h @@ -76,6 +76,11 @@ public: */ void setBufferSize(qint64 size); + /** + * @brief Set range of data to copy, if src device is not sequential + */ + void setRange(qint64 from, qint64 to); + Q_SIGNALS: /** diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f3b9dc6..0e1c324 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -7,6 +7,7 @@ set(SRC qfilesystemhandler.cpp qhttphandler.cpp qhttpparser.cpp + qhttprange.cpp qhttpserver.cpp qhttpsocket.cpp qibytearray.cpp diff --git a/src/qfilesystemhandler.cpp b/src/qfilesystemhandler.cpp index b221c9b..0c3f919 100644 --- a/src/qfilesystemhandler.cpp +++ b/src/qfilesystemhandler.cpp @@ -26,6 +26,7 @@ #include #include +#include #include #include "qfilesystemhandler_p.h" @@ -85,9 +86,35 @@ void QFilesystemHandlerPrivate::processFile(QHttpSocket *socket, const QString & connect(copier, &QIODeviceCopier::finished, copier, &QIODeviceCopier::deleteLater); connect(copier, &QIODeviceCopier::finished, file, &QFile::deleteLater); + qint64 fileSize = file->size(); + + // Checking for partial content request + QByteArray rangeHeader = socket->headers().value("Range"); + QHttpRange range; + + if(!rangeHeader.isEmpty() && rangeHeader.startsWith("bytes=")) { + // Skiping 'bytes=' - first 6 chars and spliting ranges by comma + QList rangeList = rangeHeader.mid(6).split(','); + + // Taking only first range, as multiple ranges require multipart + // reply support + range = QHttpRange(QString(rangeList.at(0)), fileSize); + } + + // If range is valid, send partial content + if(range.isValid()) { + socket->setStatusCode(QHttpSocket::PartialContent); + socket->setHeader("Content-Length", QByteArray::number(range.length())); + socket->setHeader("Content-Range", QByteArray("bytes ") + range.contentRange().toLatin1()); + copier->setRange(range.from(), range.to()); + } else { + // If range is invalid or if it is not a partial content request, + // send full file + socket->setHeader("Content-Length", QByteArray::number(fileSize)); + } + // Set the mimetype and content length socket->setHeader("Content-Type", mimeType(absolutePath)); - socket->setHeader("Content-Length", QByteArray::number(file->size())); socket->writeHeaders(); // Start the copy diff --git a/src/qhttprange.cpp b/src/qhttprange.cpp new file mode 100644 index 0000000..798bc3d --- /dev/null +++ b/src/qhttprange.cpp @@ -0,0 +1,282 @@ +/* + * Copyright (c) 2016 Aleksei Ermakov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +#include + +#include + +#include "qhttprange_p.h" + +QHttpRangePrivate::QHttpRangePrivate(QHttpRange *range) + : q(range) +{ +} + +QHttpRange::QHttpRange(const QString &range, qint64 dataSize) + : d(new QHttpRangePrivate(this)) +{ + QRegExp regExp("^(\\d*)-(\\d*)$"); + + int from = 0, to = -1; + + if(regExp.indexIn(range.trimmed()) != -1) { + QString fromStr = regExp.cap(1); + QString toStr = regExp.cap(2); + + // If both strings are empty - range is invalid. Setting to out of + // bounds range and returning. + if(fromStr.isEmpty() && toStr.isEmpty()) { + d->from = 1; + d->to = 0; + d->dataSize = -1; + return; + } + + bool okFrom = true, okTo = true; + + if(!fromStr.isEmpty()) { + from = fromStr.toInt(&okFrom); + } + if(!toStr.isEmpty()) { + to = toStr.toInt(&okTo); + } + + // If failed to parse value - set to invalid range and return. + if(!okFrom) { + d->from = 1; + d->to = 0; + d->dataSize = -1; + return; + } + + if(!okTo) { + d->from = 1; + d->to = 0; + d->dataSize = -1; + return; + } + + // In case of 'last N bytes' range (Ex.: "Range: bytes=-500"), + // set from to -to and to to -1 + if(fromStr.isEmpty()) { + from = -to; + to = -1; + } + } else { // If regexp didn't match - set to invalid range and return. + d->from = 1; + d->to = 0; + d->dataSize = -1; + return; + } + + d->from = from; + d->to = to; + d->dataSize = dataSize; +} + +QHttpRange::QHttpRange(qint64 from, qint64 to, qint64 dataSize) + : d(new QHttpRangePrivate(this)) +{ + d->from = from; + d->to = to < 0 ? -1 : to; + d->dataSize = dataSize < 0 ? -1 : dataSize; +} + +QHttpRange::QHttpRange(const QHttpRange &other, qint64 dataSize) + : d(new QHttpRangePrivate(this)) +{ + d->from = other.d->from; + d->to = other.d->to; + d->dataSize = dataSize; +} + +QHttpRange::QHttpRange() + : d(new QHttpRangePrivate(this)) +{ + d->from = 1; + d->to = 0; + d->dataSize = -1; +} + +QHttpRange::~QHttpRange() +{ + delete d; +} + +QHttpRange& QHttpRange::operator=(const QHttpRange &other) +{ + if(&other != this) { + d->from = other.d->from; + d->to = other.d->to; + d->dataSize = other.d->dataSize; + } + + return *this; +} + +qint64 QHttpRange::from() const +{ + // Last N bytes requested + if(d->from < 0 && d->dataSize != -1) { + // Check if data is smaller then requested range + if(- d->from >= d->dataSize) { + return 0; + } + return d->dataSize + d->from; + } + + // Check if d->from is bigger than d->to or d->dataSize + if((d->from > d->to && d->to != -1) || + (d->from >= d->dataSize && d->dataSize != -1)) { + return 0; + } + + return d->from; +} + +qint64 QHttpRange::to() const +{ + // Last N bytes requested + if(d->from < 0 && d->dataSize != -1) { + return d->dataSize - 1; + } + + // Skip first N bytes requested + if(d->from > 0 && d->to == -1 && d->dataSize != -1) { + return d->dataSize - 1; + } + + // Check if d->from is bigger then d->to + if(d->from > d->to && d->to != -1) { + return d->from; + } + + // When d->to overshoots dataSize + if((d->to >= d->dataSize || d->to == -1) && d->dataSize != -1) { + return d->dataSize - 1; + } + + return d->to; +} + +qint64 QHttpRange::length() const +{ + if(!isValid()) { + return -1; + } + + // Last n bytes + if(d->from < 0) { + return -(d->from); + } + + // From and to are set + if(d->to >= 0) { + return d->to - d->from + 1; + } + + // From to to end + if(d->dataSize >= 0) { + return d->dataSize - d->from; + } + + return -1; +} + +qint64 QHttpRange::dataSize() const +{ + return d->dataSize; +} + +bool QHttpRange::isValid() const +{ + // Valid cases: + // 1. "-500/1000" => from: -500, to: -1; dataSize: 1000 + // 2. "10-/1000" => from: 10, to: -1; dataSize: 1000 + // 3. "10-600/1000" => from: 10, to: 600; dataSize: 1000 + // 4. "-500/*" => from: -500, to: -1; dataSize: -1 + // 5. "10-/*" => from: 10, to: -1; dataSize: -1 + // 6. "10-600/*" => from: 10, to: 600; dataSize: -1 + + // DataSize is set + if(d->dataSize >= 0) { + if(d->from < 0) { // Last n bytes + // Check if from is in range of dataSize + if(d->dataSize + d->from >= 0) { + return true; + } + } else { + if(d->to <= -1) { // To isn't set, range is up to the end + // Check if from is in range of dataSize + if(d->from < d->dataSize) { + return true; + } + } else { // from, to and dataSize are set + if(d->from <= d->to && d->to < d->dataSize) { + return true; + } + } + } + } else { // dataSize is not set + if(d->from < 0) { // Last n bytes + return true; + } else { + if(d->to <= -1) { // To isn't set, range is up to the end + return true; + } else { // from and to are set + if(d->from <= d->to) { + return true; + } + } + } + } + + return false; +} + +QString QHttpRange::contentRange() const +{ + QString fromStr, toStr, sizeStr = "*"; + + if(d->dataSize >= 0) { + if(isValid()) { + fromStr = QString::number(from()); + toStr = QString::number(to()); + sizeStr = QString::number(dataSize()); + } else { + sizeStr = QString::number(dataSize()); + } + } else { + if(isValid()) { + fromStr = QString::number(from()); + toStr = QString::number(to()); + } else { + return ""; + } + } + + if(fromStr.isEmpty() || toStr.isEmpty()) { + return QString("*/%1").arg(sizeStr); + } + + return QString("%1-%2/%3").arg(fromStr, toStr, sizeStr); +} diff --git a/src/qhttprange_p.h b/src/qhttprange_p.h new file mode 100644 index 0000000..851f6c5 --- /dev/null +++ b/src/qhttprange_p.h @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2016 Aleksei Ermakov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +#ifndef QHTTPENGINE_QHTTPRANGEPRIVATE_H +#define QHTTPENGINE_QHTTPRANGEPRIVATE_H + +#include "QHttpEngine/qhttprange.h" + +class QHttpRangePrivate +{ +public: + + explicit QHttpRangePrivate(QHttpRange *range); + + qint64 from; + qint64 to; + qint64 dataSize; + +private: + + QHttpRange *const q; +}; + +#endif // QHTTPENGINE_QHTTPRANGEPRIVATE_H diff --git a/src/qhttpsocket.cpp b/src/qhttpsocket.cpp index 374bce8..0fa28a9 100644 --- a/src/qhttpsocket.cpp +++ b/src/qhttpsocket.cpp @@ -73,6 +73,7 @@ QByteArray QHttpSocketPrivate::statusReason(int statusCode) const { switch (statusCode) { case QHttpSocket::OK: return "OK"; + case QHttpSocket::PartialContent: return "PARTIAL CONTENT"; case QHttpSocket::MovedPermanently: return "MOVED PERMANENTLY"; case QHttpSocket::Found: return "FOUND"; case QHttpSocket::BadRequest: return "BAD REQUEST"; diff --git a/src/qiodevicecopier.cpp b/src/qiodevicecopier.cpp index c7a139d..9782679 100644 --- a/src/qiodevicecopier.cpp +++ b/src/qiodevicecopier.cpp @@ -35,7 +35,9 @@ QIODeviceCopierPrivate::QIODeviceCopierPrivate(QIODeviceCopier *copier, QIODevic q(copier), src(srcDevice), dest(destDevice), - bufferSize(DefaultBufferSize) + bufferSize(DefaultBufferSize), + rangeFrom(0), + rangeTo(-1) { } @@ -71,6 +73,12 @@ void QIODeviceCopierPrivate::nextBlock() return; } + // If range is specified (rangeTo >= 0), check if end of range is reached; + // if it is, send only part from buffer truncated by range end + if(rangeTo != -1 && src->pos() > rangeTo) { + dataRead -= src->pos() - rangeTo - 1; + } + // Write the data to the destination device if (dest->write(data.constData(), dataRead) == -1) { Q_EMIT q->error(dest->errorString()); @@ -78,10 +86,10 @@ void QIODeviceCopierPrivate::nextBlock() return; } - // Check if the end of the device has been reached - if so, - // emit the finished signal and if not, continue to read - // data at the next iteration of the event loop - if (src->atEnd()) { + // Check if the end of the device has been reached or if the end of + // the requested range is reached - if so, emit the finished signal and + // if not, continue to read data at the next iteration of the event loop + if (src->atEnd() || (rangeTo != -1 && src->pos() > rangeTo)) { Q_EMIT q->finished(); } else { QTimer::singleShot(0, this, &QIODeviceCopierPrivate::nextBlock); @@ -101,6 +109,12 @@ void QIODeviceCopier::setBufferSize(qint64 size) d->bufferSize = size; } +void QIODeviceCopier::setRange(qint64 from, qint64 to) +{ + d->rangeFrom = from; + d->rangeTo = to; +} + void QIODeviceCopier::start() { if (!d->src->isOpen()) { @@ -119,6 +133,15 @@ void QIODeviceCopier::start() } } + // If range is set and d->src is not sequential, seek to starting position + if(d->rangeFrom > 0 && !d->src->isSequential()) { + if(!d->src->seek(d->rangeFrom)) { + Q_EMIT error(tr("Unable to seek source device for specified range")); + Q_EMIT finished(); + return; + } + } + // These signals cannot be connected in the constructor since they may // begin firing before the start() method is called diff --git a/src/qiodevicecopier_p.h b/src/qiodevicecopier_p.h index a47880e..85f113d 100644 --- a/src/qiodevicecopier_p.h +++ b/src/qiodevicecopier_p.h @@ -41,6 +41,9 @@ public: qint64 bufferSize; + qint64 rangeFrom; + qint64 rangeTo; + public Q_SLOTS: void onReadyRead(); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 66bdee0..2003192 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -6,6 +6,7 @@ set(TESTS TestQFilesystemHandler TestQHttpHandler TestQHttpParser + TestQHttpRange TestQHttpServer TestQHttpSocket TestQIByteArray diff --git a/tests/TestQFilesystemHandler.cpp b/tests/TestQFilesystemHandler.cpp index 0bacdc8..dbb629e 100644 --- a/tests/TestQFilesystemHandler.cpp +++ b/tests/TestQFilesystemHandler.cpp @@ -43,6 +43,9 @@ private Q_SLOTS: void initTestCase(); + void testRangeRequests_data(); + void testRangeRequests(); + void testRequests_data(); void testRequests(); @@ -111,6 +114,85 @@ void TestQFilesystemHandler::testRequests() } } +void TestQFilesystemHandler::testRangeRequests_data() +{ + QTest::addColumn("path"); + QTest::addColumn("range"); + QTest::addColumn("statusCode"); + QTest::addColumn("contentRange"); + QTest::addColumn("data"); + + QTest::newRow("full file") + << "inside" << "" + << static_cast(QHttpSocket::OK) + << "" + << Data; + + QTest::newRow("range 0-2") + << "inside" << "0-2" + << static_cast(QHttpSocket::PartialContent) + << "bytes 0-2/4" + << Data.mid(0, 3); + + QTest::newRow("range 1-2") + << "inside" << "1-2" + << static_cast(QHttpSocket::PartialContent) + << "bytes 1-2/4" + << Data.mid(1, 2); + + QTest::newRow("skip first 1 byte") + << "inside" << "1-" + << static_cast(QHttpSocket::PartialContent) + << "bytes 1-3/4" + << Data.mid(1); + + QTest::newRow("last 2 bytes") + << "inside" << "-2" + << static_cast(QHttpSocket::PartialContent) + << "bytes 2-3/4" + << Data.mid(2); + + QTest::newRow("bad range request") + << "inside" << "abcd" + << static_cast(QHttpSocket::OK) + << "" + << Data; +} + +void TestQFilesystemHandler::testRangeRequests() +{ + QFETCH(QString, path); + QFETCH(QString, range); + QFETCH(int, statusCode); + QFETCH(QString, contentRange); + QFETCH(QByteArray, data); + + QFilesystemHandler handler(QDir(dir.path()).absoluteFilePath("root")); + + QSocketPair pair; + QTRY_VERIFY(pair.isConnected()); + + QSimpleHttpClient client(pair.client()); + QHttpSocket socket(pair.server(), &pair); + + if (!range.isEmpty()) { + QHttpSocket::QHttpHeaderMap inHeaders; + inHeaders.insert("Range", QByteArray("bytes=") + range.toUtf8()); + client.sendHeaders("GET", path.toUtf8(), inHeaders); + QTRY_VERIFY(socket.isHeadersParsed()); + } + + handler.route(&socket, path); + + QTRY_COMPARE(client.statusCode(), statusCode); + + if (!data.isNull()) { + QTRY_COMPARE(client.data(), data); + QCOMPARE(client.headers().value("Content-Length").toInt(), data.length()); + QCOMPARE(client.headers().value("Content-Range"), contentRange.toLatin1()); + } +} + bool TestQFilesystemHandler::createFile(const QString &path) { QFile file(QDir(dir.path()).absoluteFilePath(path)); diff --git a/tests/TestQHttpRange.cpp b/tests/TestQHttpRange.cpp new file mode 100644 index 0000000..3a8e2d7 --- /dev/null +++ b/tests/TestQHttpRange.cpp @@ -0,0 +1,291 @@ +/* + * Copyright (c) 2016 Aleksei Ermakov + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +#include +#include +#include + +#include + +class TestQHttpRange : public QObject +{ + Q_OBJECT + +public: + + TestQHttpRange(); + +private Q_SLOTS: + + void testDefaultConstructor(); + void testAssignmentOperator(); + + void testFromToLength_data(); + void testFromToLength(); + + void testIsValid_data(); + void testIsValid(); + + void testParseFromString_data(); + void testParseFromString(); + + void testContentRange_data(); + void testContentRange(); +}; + +TestQHttpRange::TestQHttpRange() +{ +} + +void TestQHttpRange::testDefaultConstructor() +{ + QHttpRange range; + + QCOMPARE(range.isValid(), false); +} + +void TestQHttpRange::testAssignmentOperator() +{ + QHttpRange range; + QHttpRange otherRange(100, 200, -1); + + range = otherRange; + + QCOMPARE(range.isValid(), true); + QCOMPARE(range.from(), 100); + QCOMPARE(range.to(), 200); +} + +void TestQHttpRange::testFromToLength_data() +{ + QTest::addColumn("inFrom"); + QTest::addColumn("inTo"); + QTest::addColumn("inDataSize"); + QTest::addColumn("from"); + QTest::addColumn("to"); + QTest::addColumn("length"); + + QTest::newRow("Last 500 bytes") + << -500 << -1 << -1 + << -500 << -1 << 500; + + QTest::newRow("Last 500 bytes with 800 dataSize") + << -500 << -1 << 800 + << 300 << 799 << 500; + + QTest::newRow("Skip first 10 bytes") + << 10 << -1 << -1 + << 10 << -1 << -1; + + QTest::newRow("Skip first 10 bytes with 100 dataSize") + << 10 << -1 << 100 + << 10 << 99 << 90; +} + +void TestQHttpRange::testFromToLength() +{ + QFETCH(int, inFrom); + QFETCH(int, inTo); + QFETCH(int, inDataSize); + QFETCH(int, from); + QFETCH(int, to); + QFETCH(int, length); + + QHttpRange range(inFrom, inTo, inDataSize); + + QCOMPARE(range.from(), from); + QCOMPARE(range.to(), to); + QCOMPARE(range.length(), length); +} + +void TestQHttpRange::testIsValid_data() +{ + QTest::addColumn("from"); + QTest::addColumn("to"); + QTest::addColumn("dataSize"); + QTest::addColumn("valid"); + + QTest::newRow("Normal range") + << 0 << 100 << -1 << true; + + QTest::newRow("Normal range with 'dataSize'") + << 0 << 99 << 100 << true; + + QTest::newRow("Last N bytes") + << -500 << -1 << -1 << true; + + QTest::newRow("Last N bytes with 'dataSize'") + << -500 << -1 << 500 << true; + + QTest::newRow("Skip first N bytes") + << 10 << -1 << -1 << true; + + QTest::newRow("Skip first N bytes with 'dataSize'") + << 10 << -1 << 500 << true; + + QTest::newRow("OutOfBounds 'to' > 'from'") + << 100 << 50 << -1 << false; + + QTest::newRow("OutOfBounds 'from' > 'dataSize'") + << 100 << 200 << 150 << false; + + QTest::newRow("Last N bytes where N > 'dataSize'") + << -500 << -1 << 499 << false; + + QTest::newRow("Skip first N bytes where N > 'dataSize'") + << 500 << -1 << 499 << false; +} + +void TestQHttpRange::testIsValid() +{ + QFETCH(int, from); + QFETCH(int, to); + QFETCH(int, dataSize); + QFETCH(bool, valid); + + QHttpRange range(from, to, dataSize); + + QCOMPARE(range.isValid(), valid); +} + +void TestQHttpRange::testParseFromString_data() +{ + QTest::addColumn("data"); + QTest::addColumn("dataSize"); + QTest::addColumn("valid"); + QTest::addColumn("from"); + QTest::addColumn("to"); + QTest::addColumn("length"); + + QTest::newRow("Normal range") + << "0-99" << -1 + << true << 0 << 99 << 100; + + QTest::newRow("Normal range with 'dataSize'") + << "0-99" << 100 + << true << 0 << 99 << 100; + + QTest::newRow("Last N bytes") + << "-256" << -1 + << true << -256 << -1 << 256; + + QTest::newRow("Last N bytes with 'dataSize'") + << "-256" << 256 + << true << 0 << 255 << 256; + + QTest::newRow("Skip first N bytes") + << "100-" << -1 + << true << 100 << -1 << -1; + + QTest::newRow("Skip first N bytes with 'dataSize'") + << "100-" << 200 + << true << 100 << 199 << 100; + + QTest::newRow("OutOfBounds 'to' > 'from'") + << "100-50" << -1 + << false; + + QTest::newRow("OutOfBounds 'from' > 'dataSize'") + << "0-200" << 100 + << false; + + QTest::newRow("Last N bytes where N > 'dataSize'") + << "-500" << 200 + << false; + + QTest::newRow("Skip first N bytes where N > 'dataSize'") + << "100-" << 100 + << false; + + QTest::newRow("Bad input: '-'") + << "-" << -1 + << false; + + QTest::newRow("Bad input: 'abc-def'") + << "abc-def" << -1 + << false; + + QTest::newRow("Bad input: 'abcdef'") + << "abcdef" << -1 + << false; +} + +void TestQHttpRange::testParseFromString() +{ + QFETCH(QString, data); + QFETCH(int, dataSize); + QFETCH(bool, valid); + + QHttpRange range(data, dataSize); + + QCOMPARE(range.isValid(), valid); + + if(valid) { + QFETCH(int, from); + QFETCH(int, to); + QFETCH(int, length); + + QCOMPARE(range.from(), from); + QCOMPARE(range.to(), to); + QCOMPARE(range.length(), length); + } +} + +void TestQHttpRange::testContentRange_data() +{ + QTest::addColumn("from"); + QTest::addColumn("to"); + QTest::addColumn("dataSize"); + QTest::addColumn("contentRange"); + + QTest::newRow("Normal range with 'dataSize'") + << 0 << 100 << 1000 + << "0-100/1000"; + + QTest::newRow("Normal range without 'dataSize'") + << 0 << 100 << -1 + << "0-100/*"; + + QTest::newRow("Invalid range with 'dataSize'") + << 100 << 10 << 1200 + << "*/1200"; + + QTest::newRow("Invalid range without 'dataSize'") + << 100 << 10 << -1 + << ""; +} + +void TestQHttpRange::testContentRange() +{ + QFETCH(int, from); + QFETCH(int, to); + QFETCH(int, dataSize); + QFETCH(QString, contentRange); + + QHttpRange range(from, to, dataSize); + + QCOMPARE(range.contentRange(), contentRange); +} + + +QTEST_MAIN(TestQHttpRange) +#include "TestQHttpRange.moc" diff --git a/tests/TestQIODeviceCopier.cpp b/tests/TestQIODeviceCopier.cpp index fe6a34c..2d4fd15 100644 --- a/tests/TestQIODeviceCopier.cpp +++ b/tests/TestQIODeviceCopier.cpp @@ -31,7 +31,7 @@ #include "common/qsocketpair.h" -const QByteArray SampleData = "1234567890"; +const QByteArray SampleData = "1234567890123456789012345678901234567890"; class TestQIODeviceCopier : public QObject { @@ -39,6 +39,9 @@ class TestQIODeviceCopier : public QObject private Q_SLOTS: + void testRange_data(); + void testRange(); + void testQBuffer(); void testQTcpSocket(); void testStop(); @@ -110,5 +113,47 @@ void TestQIODeviceCopier::testStop() QTRY_COMPARE(destData, SampleData); } +void TestQIODeviceCopier::testRange_data() +{ + QTest::addColumn("from"); + QTest::addColumn("to"); + QTest::addColumn("bufferSize"); + + QTest::newRow("range: 1-21, bufSize: 8") + << 1 << 21 << 8; + + QTest::newRow("range: 0-21, bufSize: 7") + << 0 << 21 << 7; + + QTest::newRow("range: 10-, bufSize: 5") + << 10 << -1 << 5; +} + +void TestQIODeviceCopier::testRange() +{ + QFETCH(int, from); + QFETCH(int, to); + QFETCH(int, bufferSize); + + QBuffer src; + src.setData(SampleData); + + QByteArray destData; + QBuffer dest(&destData); + + QIODeviceCopier copier(&src, &dest); + copier.setBufferSize(bufferSize); + copier.setRange(from, to); + + QSignalSpy errorSpy(&copier, SIGNAL(error(QString))); + QSignalSpy finishedSpy(&copier, SIGNAL(finished())); + + copier.start(); + + QTRY_COMPARE(finishedSpy.count(), 1); + QCOMPARE(errorSpy.count(), 0); + QCOMPARE(destData, SampleData.mid(from, to - from + 1)); +} + QTEST_MAIN(TestQIODeviceCopier) #include "TestQIODeviceCopier.moc"