Refactor gzip decompression to share common code for chunked support. PiperOrigin-RevId: 914468148
diff --git a/quiche/quic/masque/masque_ohttp_client.cc b/quiche/quic/masque/masque_ohttp_client.cc index 9c0c05a..e8d8056 100644 --- a/quiche/quic/masque/masque_ohttp_client.cc +++ b/quiche/quic/masque/masque_ohttp_client.cc
@@ -104,51 +104,6 @@ return chunks; } -absl::StatusOr<std::string> GzipDecompress(absl::string_view input) { - z_stream zs; - memset(&zs, 0, sizeof(zs)); - - // Initialize zlib for gzip decompression. - // 16 + MAX_WBITS tells zlib to expect a gzip header and trailer, which is the - // expectation for valid HTTP responses with `Content-Encoding: gzip`. - if (inflateInit2(&zs, 16 + MAX_WBITS) != Z_OK) { - return absl::InternalError( - "Failed to initialize zlib for gzip decompression"); - } - - // Automatically clean up zlib resources when exiting the function. - absl::Cleanup cleanup = [&zs] { inflateEnd(&zs); }; - - zs.next_in = reinterpret_cast<uint8_t*>(const_cast<char*>(input.data())); - zs.avail_in = input.size(); - - int ret; - std::vector<uint8_t> outbuffer(kGzipDecompressBufferSize); - std::string decompressed; - - // Decompress the input in chunks until we reach the end of the stream. - do { - zs.next_out = outbuffer.data(); - zs.avail_out = outbuffer.size(); - - ret = inflate(&zs, Z_NO_FLUSH); - - // Calculate how much data was placed in the buffer and append it. - size_t decompressed_size = outbuffer.size() - zs.avail_out; - decompressed.append(reinterpret_cast<char*>(outbuffer.data()), - decompressed_size); - } while (ret == Z_OK); - - // Z_STREAM_END indicates that the full compressed stream was processed - // successfully. - if (ret != Z_STREAM_END) { - return absl::InternalError( - absl::StrCat("Gzip decompression failed with error code: ", ret)); - } - - return decompressed; -} - class PingPongResponseVisitor : public MasqueOhttpClient::ResponseVisitor { public: explicit PingPongResponseVisitor(std::vector<std::string> chunks) @@ -246,6 +201,92 @@ } // namespace +absl::StatusOr<GzipDecompressor::DecompressedData> +GzipDecompressor::InflateLoop(int flush) { + int ret = Z_OK; + std::string decompressed = ""; + std::vector<unsigned char> outbuffer(kGzipDecompressBufferSize); + + // Decompress the input in chunks until we reach the end of the stream or + // need more input. + do { + stream_->next_out = outbuffer.data(); + stream_->avail_out = outbuffer.size(); + + ret = inflate(stream_.get(), flush); + + if (ret != Z_OK && ret != Z_STREAM_END) { + return absl::InternalError(absl::StrCat( + "Gzip decompression failed with error code: ", ret, + stream_->msg ? absl::StrCat(" (", stream_->msg, ")") : "")); + } + + // Calculate how much data was placed in the buffer. + size_t decompressed_size = outbuffer.size() - stream_->avail_out; + decompressed.append(reinterpret_cast<char*>(outbuffer.data()), + decompressed_size); + } while (stream_->avail_out == 0 && ret != Z_STREAM_END); + + return DecompressedData{ret, std::move(decompressed)}; +} + +absl::StatusOr<std::unique_ptr<GzipDecompressor>> GzipDecompressor::Create() { + auto stream = std::make_unique<z_stream>(); + // z_stream fields must be explicitly initialized, as per the zlib + // documentation. + memset(stream.get(), 0, sizeof(z_stream)); + + // Initialize zlib for gzip decompression. + // 16 + MAX_WBITS tells zlib to expect a gzip header and trailer, which is + // the expectation for valid HTTP responses with `Content-Encoding: gzip`. + int inflate_result = inflateInit2(stream.get(), 16 + MAX_WBITS); + if (inflate_result != Z_OK) { + return absl::InternalError( + absl::StrCat("Failed to initialize zlib for gzip decompression, error " + "code: ", + inflate_result)); + } + + return std::unique_ptr<GzipDecompressor>( + new GzipDecompressor(std::move(stream))); +} + +absl::StatusOr<std::string> GzipDecompressor::Decompress( + absl::string_view input, bool end_stream) { + if (stream_ == nullptr) { + return absl::FailedPreconditionError("Decompression has already ended"); + } + stream_->next_in = + reinterpret_cast<unsigned char*>(const_cast<char*>(input.data())); + stream_->avail_in = input.size(); + + QUICHE_ASSIGN_OR_RETURN(DecompressedData decompressed_data, + InflateLoop(end_stream ? Z_FINISH : Z_NO_FLUSH)); + + if (decompressed_data.status_code == Z_STREAM_END) { + finished_ = true; + } + + if (end_stream && !finished_) { + return absl::InternalError( + "Gzip decompression failed to reach end of stream"); + } + return decompressed_data.data; +} + +void GzipDecompressor::EndDecompression() { + if (stream_ != nullptr) { + inflateEnd(stream_.get()); + stream_.reset(); + } +} + +absl::StatusOr<std::string> GzipDecompress(absl::string_view input) { + QUICHE_ASSIGN_OR_RETURN(std::unique_ptr<GzipDecompressor> decompressor, + GzipDecompressor::Create()); + return decompressor->Decompress(input, /*end_stream=*/true); +} + std::string MasqueOhttpClient::Config::PerRequestConfig::method() const { if (method_.has_value()) { return *method_;
diff --git a/quiche/quic/masque/masque_ohttp_client.h b/quiche/quic/masque/masque_ohttp_client.h index 132625e..6295b31 100644 --- a/quiche/quic/masque/masque_ohttp_client.h +++ b/quiche/quic/masque/masque_ohttp_client.h
@@ -26,9 +26,55 @@ #include "quiche/oblivious_http/buffers/oblivious_http_request.h" #include "quiche/oblivious_http/common/oblivious_http_chunk_handler.h" #include "quiche/oblivious_http/oblivious_http_client.h" +#include <zlib.h> namespace quic { +// A class that decompresses gzip encoded data using zlib. This also supports +// incremental decompression, enabling the caller to feed a stream of compressed +// chunks into the decompressor one by one. +class QUICHE_NO_EXPORT GzipDecompressor { + public: + // Not copyable. + GzipDecompressor(const GzipDecompressor&) = delete; + GzipDecompressor& operator=(const GzipDecompressor&) = delete; + + // Destructor is needed to free the internal state + // allocated by zlib during `inflateInit2`. + ~GzipDecompressor() { EndDecompression(); } + + static absl::StatusOr<std::unique_ptr<GzipDecompressor>> Create(); + + // Can be called multiple times with continuous input chunks. `end_stream` + // should be set to true when the input is confirmed to have the final chunk. + absl::StatusOr<std::string> Decompress(absl::string_view input, + bool end_stream); + + bool IsFinished() const { return finished_; } + + // This will free the internal state allocated by zlib. The decompressor + // should not be used after this is called. + void EndDecompression(); + + private: + struct DecompressedData { + int status_code; + std::string data; + }; + + explicit GzipDecompressor(std::unique_ptr<z_stream> stream) + : stream_(std::move(stream)) {} + + // Decompresses data from the zlib stream. + // `flush` determines the flushing behavior for zlib (e.g., Z_NO_FLUSH for + // chunks, Z_FINISH for single non-chunked). Returns the decompressed data and + // the zlib return code (e.g., Z_OK, Z_STREAM_END). + absl::StatusOr<DecompressedData> InflateLoop(int flush); + + std::unique_ptr<z_stream> stream_; + bool finished_ = false; +}; + // A client that sends OHTTP requests through a relay/gateway to target URLs. class QUICHE_EXPORT MasqueOhttpClient : public quic::MasqueConnectionPool::Visitor {