Merge "util: fix brotli decompression of empty input" into main

This commit is contained in:
jade 2024-09-18 23:36:25 +00:00 committed by Gerrit Code Review
commit 79246a3733
4 changed files with 55 additions and 6 deletions

View file

@ -337,7 +337,7 @@ struct curlFileTransfer : public FileTransfer
// wrapping user `callback`s instead is not possible because the // wrapping user `callback`s instead is not possible because the
// Callback api expects std::functions, and copying Callbacks is // Callback api expects std::functions, and copying Callbacks is
// not possible due the promises they hold. // not possible due the promises they hold.
if (code == CURLE_OK && !dataCallback) { if (code == CURLE_OK && !dataCallback && result.data.length() > 0) {
result.data = decompress(encoding, result.data); result.data = decompress(encoding, result.data);
} }

View file

@ -144,6 +144,7 @@ struct BrotliDecompressionSource : Source
std::unique_ptr<char[]> buf; std::unique_ptr<char[]> buf;
size_t avail_in = 0; size_t avail_in = 0;
const uint8_t * next_in; const uint8_t * next_in;
std::exception_ptr inputEofException = nullptr;
Source * inner; Source * inner;
std::unique_ptr<BrotliDecoderState, void (*)(BrotliDecoderState *)> state; std::unique_ptr<BrotliDecoderState, void (*)(BrotliDecoderState *)> state;
@ -167,23 +168,42 @@ struct BrotliDecompressionSource : Source
while (len && !BrotliDecoderIsFinished(state.get())) { while (len && !BrotliDecoderIsFinished(state.get())) {
checkInterrupt(); checkInterrupt();
while (avail_in == 0) { while (avail_in == 0 && inputEofException == nullptr) {
try { try {
avail_in = inner->read(buf.get(), BUF_SIZE); avail_in = inner->read(buf.get(), BUF_SIZE);
} catch (EndOfFile &) { } catch (EndOfFile &) {
// No more data, but brotli may still have output remaining
// from the last call.
inputEofException = std::current_exception();
break; break;
} }
next_in = charptr_cast<const uint8_t *>(buf.get()); next_in = charptr_cast<const uint8_t *>(buf.get());
} }
if (!BrotliDecoderDecompressStream( BrotliDecoderResult res = BrotliDecoderDecompressStream(
state.get(), &avail_in, &next_in, &len, &out, nullptr state.get(), &avail_in, &next_in, &len, &out, nullptr
)) );
{
switch (res) {
case BROTLI_DECODER_RESULT_SUCCESS:
// We're done here!
goto finish;
case BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT:
// Grab more input. Don't try if we already have exhausted our input stream.
if (inputEofException != nullptr) {
std::rethrow_exception(inputEofException);
} else {
continue;
}
case BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT:
// Need more output space: we can only get another buffer by someone calling us again, so get out.
goto finish;
case BROTLI_DECODER_RESULT_ERROR:
throw CompressionError("error while decompressing brotli file"); throw CompressionError("error while decompressing brotli file");
} }
} }
finish:
if (begin != out) { if (begin != out) {
return out - begin; return out - begin;
} else { } else {

View file

@ -77,6 +77,11 @@ struct Source
* Store up to len in the buffer pointed to by data, and * Store up to len in the buffer pointed to by data, and
* return the number of bytes stored. It blocks until at least * return the number of bytes stored. It blocks until at least
* one byte is available. * one byte is available.
*
* Should not return 0 (generally you want to throw EndOfFile), but nothing
* stops that.
*
* \throws EndOfFile if there is no more data.
*/ */
virtual size_t read(char * data, size_t len) = 0; virtual size_t read(char * data, size_t len) = 0;

View file

@ -136,4 +136,28 @@ TEST_P(PerTypeNonNullCompressionTest, bogusInputDecompression)
auto bogus = "this data is bogus and should throw when decompressing"; auto bogus = "this data is bogus and should throw when decompressing";
ASSERT_THROW(decompress(param, bogus), CompressionError); ASSERT_THROW(decompress(param, bogus), CompressionError);
} }
TEST_P(PerTypeNonNullCompressionTest, truncatedValidInput)
{
auto method = GetParam();
auto inputString = "the quick brown fox jumps over the lazy doggos";
auto compressed = compress(method, inputString);
/* n.b. This also tests zero-length input, which is also invalid.
* As of the writing of this comment, it returns empty output, but is
* allowed to throw a compression error instead. */
for (int i = 0; i < compressed.length(); ++i) {
auto newCompressed = compressed.substr(compressed.length() - i);
try {
decompress(method, newCompressed);
// Success is acceptable as well, even though it is corrupt data.
// The compression method is not expected to provide integrity,
// just, not break explosively on bad input.
} catch (CompressionError &) {
// Acceptable
}
}
}
} }