diff --git a/src/corelib/io/qiodevice.cpp b/src/corelib/io/qiodevice.cpp index 06884ef2758..3710df8bee2 100644 --- a/src/corelib/io/qiodevice.cpp +++ b/src/corelib/io/qiodevice.cpp @@ -86,6 +86,14 @@ static void checkWarnMessage(const QIODevice *device, const char *function, cons } \ } while (0) +#define CHECK_LINEMAXLEN_1(function, returnType) \ + do { \ + if (maxSize < 1) { \ + checkWarnMessage(this, #function, "Called with maxSize < 1"); \ + return returnType; \ + } \ + } while (0) + #define CHECK_MAXBYTEARRAYSIZE(function) \ do { \ if (maxSize >= QByteArray::maxSize()) { \ @@ -1335,13 +1343,17 @@ qint64 QIODevice::readLine(char *data, qint64 maxSize) /*! \internal */ -qint64 QIODevicePrivate::readLine(char *data, qint64 maxSize) +qint64 QIODevicePrivate::readLine(char *data, qint64 maxSize, ReadLineOption option) { Q_Q(QIODevice); - Q_ASSERT(maxSize >= 2); + const auto appendNullByte = option & ReadLineOption::NullTerminated; - // Leave room for a '\0' - --maxSize; + if (appendNullByte) { + Q_ASSERT(maxSize >= 2); + --maxSize; // Leave room for a '\0' + } else { + Q_ASSERT(maxSize >= 1); + } const bool sequential = isSequential(); const bool keepDataInBuffer = sequential && transactionStarted; @@ -1358,8 +1370,8 @@ qint64 QIODevicePrivate::readLine(char *data, qint64 maxSize) q->readData(data, 0); } } else if (!buffer.isEmpty()) { - // QRingBuffer::readLine() terminates the line with '\0' - readSoFar = buffer.readLine(data, maxSize + 1); + // QRingBuffer::readLine() terminates the line with '\0' if requested + readSoFar = buffer.readLine(data, maxSize + (appendNullByte ? 1 : 0), option); if (buffer.isEmpty()) q->readData(data, 0); if (!sequential) @@ -1380,7 +1392,9 @@ qint64 QIODevicePrivate::readLine(char *data, qint64 maxSize) data[readSoFar - 1] = '\n'; } } - data[readSoFar] = '\0'; + if (appendNullByte) + data[readSoFar] = '\0'; + return readSoFar; } } @@ -1401,7 +1415,8 @@ qint64 QIODevicePrivate::readLine(char *data, qint64 maxSize) } #endif if (readBytes < 0) { - data[readSoFar] = '\0'; + if (appendNullByte) + data[readSoFar] = '\0'; return readSoFar ? readSoFar : -1; } readSoFar += readBytes; @@ -1411,12 +1426,14 @@ qint64 QIODevicePrivate::readLine(char *data, qint64 maxSize) // assume the device position is invalid and force a seek. devicePos = qint64(-1); } - data[readSoFar] = '\0'; + if (appendNullByte) + data[readSoFar] = '\0'; if (openMode & QIODevice::Text) { if (readSoFar > 1 && data[readSoFar - 1] == '\n' && data[readSoFar - 2] == '\r') { data[readSoFar - 2] = '\n'; - data[readSoFar - 1] = '\0'; + if (appendNullByte) + data[readSoFar - 1] = '\0'; --readSoFar; } } @@ -1527,6 +1544,64 @@ bool QIODevice::readLineInto(QByteArray *line, qint64 maxSize) return true; } +/*! + \since 6.9 + + \fn QIODevice::readLineInto(QSpan buffer); + \fn QIODevice::readLineInto(QSpan buffer); + \fn QIODevice::readLineInto(QSpan buffer); + + Reads a line from this device into \a buffer, and returns the subset of + \a buffer that contains the data read. + + If \a buffer's size is smaller than the length of the line, only the + characters that fit within \a buffer are read and returned. In this case, + calling readLineInto() again will retrieve the remainder of the line. + To determine whether the entire line was read, first check if the device is + atEnd(), in case the last line didn't end with a newline. If not atEnd(), + verify whether the returned view ends with '\n'. Otherwise, another call to + readLineInto() is required. + + The resulting line can have trailing end-of-line characters ("\n" or "\r\n"), + so calling QByteArrayView::trimmed() may be necessary. + + In case an error occurred, this function returns a null QByteArrayView. + Otherwise it is a sub-span of \a buffer. If no data was currently + available for reading or the device is atEnd(), this function returns an + empty QByteArrayView. + + Note that the return value is not null terminated. If you want + null-termination, you can pass \c{buffer.chopped(1)} and then insert '\\0' + at \c{buffer[result.size()]}. + + \sa readLine() +*/ +QByteArrayView QIODevice::readLineInto(QSpan buffer) +{ + Q_D(QIODevice); + qint64 maxSize = buffer.size(); +#if defined QIODEVICE_DEBUG + printf("%p QIODevice::readLineInto(%lld), d->pos = %lld, d->buffer.size() = %lld\n", + this, qlonglong(maxSize), qlonglong(d->pos), qlonglong(d->buffer.size())); +#endif + + CHECK_READABLE(readLineInto, {}); + + if (atEnd()) + return buffer.first(0); + + CHECK_LINEMAXLEN_1(readLineInto, {}); + CHECK_MAXBYTEARRAYSIZE(readLineInto); + + const qint64 readBytes = d->readLine(reinterpret_cast(buffer.data()), buffer.size(), + QIODevicePrivate::ReadLineOption::NotNullTerminated); + + if (readBytes < 0) + return {}; + + return buffer.first(readBytes); +} + /*! Reads up to \a maxSize characters into \a data and returns the number of characters read. diff --git a/src/corelib/io/qiodevice.h b/src/corelib/io/qiodevice.h index 0c9d684fc79..76bc61e7325 100644 --- a/src/corelib/io/qiodevice.h +++ b/src/corelib/io/qiodevice.h @@ -12,6 +12,7 @@ #include #include #endif +#include #include #ifdef open @@ -79,6 +80,13 @@ public: qint64 readLine(char *data, qint64 maxlen); QByteArray readLine(qint64 maxlen = 0); bool readLineInto(QByteArray *result, qint64 maxlen = 0); + + QByteArrayView readLineInto(QSpan buffer) + { return readLineInto(as_writable_bytes(buffer)); } + QByteArrayView readLineInto(QSpan buffer) + { return readLineInto(as_writable_bytes(buffer)); } + QByteArrayView readLineInto(QSpan buffer); + virtual bool canReadLine() const; void startTransaction(); diff --git a/src/corelib/io/qiodevice_p.h b/src/corelib/io/qiodevice_p.h index d9bee17bf51..9f45e828312 100644 --- a/src/corelib/io/qiodevice_p.h +++ b/src/corelib/io/qiodevice_p.h @@ -45,6 +45,12 @@ public: QIODevicePrivate(); virtual ~QIODevicePrivate(); + enum class ReadLineOption { + NotNullTerminated, + NullTerminated, + }; + Q_DECLARE_FLAGS(ReadLineOptions, ReadLineOption) + // The size of this class is a subject of the library hook data. // When adding a new member, do not make gaps and be aware // about the padding. Accordingly, adjust offsets in @@ -85,7 +91,14 @@ public: inline void append(const char *data, qint64 size) { Q_ASSERT(m_buf); m_buf->append(data, size); } inline void append(const QByteArray &qba) { Q_ASSERT(m_buf); m_buf->append(qba); } inline qint64 skip(qint64 length) { return (m_buf ? m_buf->skip(length) : Q_INT64_C(0)); } - inline qint64 readLine(char *data, qint64 maxLength) { return (m_buf ? m_buf->readLine(data, maxLength) : Q_INT64_C(-1)); } + qint64 readLine(char *data, qint64 maxLength, + ReadLineOptions option = ReadLineOption::NullTerminated) + { + const auto appendNullByte = option & ReadLineOption::NullTerminated; + return !m_buf ? Q_INT64_C(-1) : + appendNullByte ? m_buf->readLine(data, maxLength) : + m_buf->readLineWithoutTerminatingNull(data, maxLength); + } inline bool canReadLine() const { return m_buf && m_buf->canReadLine(); } }; @@ -145,7 +158,9 @@ public: void setWriteChannelCount(int count); qint64 read(char *data, qint64 maxSize, bool peeking = false); - qint64 readLine(char *data, qint64 maxSize); + qint64 readLine(char *data, qint64 maxSize, + ReadLineOption option = ReadLineOption::NullTerminated); + virtual qint64 peek(char *data, qint64 maxSize); virtual QByteArray peek(qint64 maxSize); qint64 skipByReading(qint64 maxSize); @@ -164,6 +179,8 @@ public: #endif }; +Q_DECLARE_OPERATORS_FOR_FLAGS(QIODevicePrivate::ReadLineOptions) + QT_END_NAMESPACE #endif // QIODEVICE_P_H diff --git a/src/corelib/tools/qringbuffer.cpp b/src/corelib/tools/qringbuffer.cpp index 66de1f59c06..f6ba5706993 100644 --- a/src/corelib/tools/qringbuffer.cpp +++ b/src/corelib/tools/qringbuffer.cpp @@ -340,14 +340,22 @@ void QRingBuffer::append(QByteArray &&qba) bufferSize += qbaSize; } -qint64 QRingBuffer::readLine(char *data, qint64 maxLength) +qint64 QRingBuffer::readLineWithoutTerminatingNull(char *data, qint64 maxLength) { - Q_ASSERT(data != nullptr && maxLength > 1); + Q_ASSERT(data != nullptr && maxLength > 0); - --maxLength; qint64 i = indexOf('\n', maxLength); i = read(data, i >= 0 ? (i + 1) : maxLength); + return i; +} + +qint64 QRingBuffer::readLine(char *data, qint64 maxLength) +{ + Q_ASSERT(maxLength > 1); + + qint64 i = readLineWithoutTerminatingNull(data, maxLength - 1); + // Terminate it. data[i] = '\0'; return i; diff --git a/src/corelib/tools/qringbuffer_p.h b/src/corelib/tools/qringbuffer_p.h index 25113213c90..1ff7c59ed9b 100644 --- a/src/corelib/tools/qringbuffer_p.h +++ b/src/corelib/tools/qringbuffer_p.h @@ -214,6 +214,7 @@ public: return bytesToSkip; } + Q_CORE_EXPORT qint64 readLineWithoutTerminatingNull(char *data, qint64 maxLength); Q_CORE_EXPORT qint64 readLine(char *data, qint64 maxLength); inline bool canReadLine() const { diff --git a/tests/auto/corelib/io/qiodevice/tst_qiodevice.cpp b/tests/auto/corelib/io/qiodevice/tst_qiodevice.cpp index 68cde500889..bd147b65329 100644 --- a/tests/auto/corelib/io/qiodevice/tst_qiodevice.cpp +++ b/tests/auto/corelib/io/qiodevice/tst_qiodevice.cpp @@ -32,6 +32,7 @@ private slots: void readLineInto_Checks(); void readLineInto(); + void readLineInto_qspan(); void readAllKeepPosition(); void writeInTextMode(); @@ -628,6 +629,75 @@ void tst_QIODevice::readLineInto() QCOMPARE(buffer.pos(), pos_before); } +void tst_QIODevice::readLineInto_qspan() +{ + QByteArray data ("1st Line\r\nL2\r\nRead the rest"); + QBuffer buffer(&data); + + { + QVERIFY(buffer.open(QIODevice::ReadOnly)); + QVERIFY(buffer.canReadLine()); + buffer.seek(0); + + QSpan span; // zero-sized span + QTest::ignoreMessage(QtWarningMsg, + "QIODevice::readLineInto (QBuffer): Called with maxSize < 1"); + QCOMPARE(buffer.readLineInto(span), ""); + + char buffer1[1024]; + QCOMPARE(buffer.readLineInto(buffer1), "1st Line\r\n"); + + uchar buffer2[4]; // length of the buffer is equal to the size of the line + QCOMPARE(buffer.readLineInto(buffer2), "L2\r\n"); + + std::byte buffer3[5]; // length of the buffer is less than the size of the line + QCOMPARE(buffer.readLineInto(buffer3), "Read "); + QCOMPARE(buffer.readLineInto(buffer1), "the rest"); // read the rest + + QCOMPARE(buffer.readLineInto(span), ""); // No warning even thought maxSize < 1 because we + // are at the end + } + + { + QVERIFY(buffer.open(QIODevice::ReadOnly | QIODevice::Text)); // "\r\n" is translated to '\n' + QVERIFY(buffer.canReadLine()); + buffer.seek(0); + + QSpan span; // zero-sized span + QTest::ignoreMessage(QtWarningMsg, + "QIODevice::readLineInto (QBuffer): Called with maxSize < 1"); + QCOMPARE(buffer.readLineInto(span), ""); + + char buffer1[1024]; + QCOMPARE(buffer.readLineInto(buffer1), "1st Line\n"); + + uchar buffer2[3]; // length of the buffer is equal to the size of the line + QCOMPARE(buffer.readLineInto(buffer2), "L2\n"); + + std::byte buffer3[5]; // length of the buffer is less than the size of the line + QCOMPARE(buffer.readLineInto(buffer3), "Read "); + QCOMPARE(buffer.readLineInto(buffer1), "the rest"); // read the rest + + QCOMPARE(buffer.readLineInto(span), ""); // No warning even thought maxSize < 1 because we + // are at the end + } + + { + // This test checks the behavior when !keepDataInBuffer and !buffer.isEmpty(). + // 'buffer' was always empty in the previous tests and calling ungetChar() changes that. + QByteArray data2 ("Q"); + QBuffer buffer2(&data2); + QVERIFY(buffer2.open(QIODevice::ReadOnly)); + buffer2.seek(0); + buffer2.read(1); + buffer2.ungetChar('t'); // Make the buffer size equal to 1 + + char buf[1]; + QCOMPARE(buffer2.readLineInto(buf), "t"); + QCOMPARE(buffer2.readLineInto(buf), ""); // no more data to read + } +} + class SequentialReadBuffer : public QIODevice { public: