Skip to content

Commit 0406daf

Browse files
committed
Add support for decompression in StreamHandle with tests for gzip, Brotli, and zstd
1 parent 4c27441 commit 0406daf

File tree

2 files changed

+398
-19
lines changed

2 files changed

+398
-19
lines changed

httplib.h

Lines changed: 110 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1478,6 +1478,9 @@ struct BodyReader {
14781478
bool has_error() const { return last_error != Error::Success; }
14791479
};
14801480

1481+
// Forward declaration for compression support in StreamHandle
1482+
class decompressor;
1483+
14811484
} // namespace detail
14821485

14831486
class ClientImpl {
@@ -1509,6 +1512,11 @@ class ClientImpl {
15091512
Stream *stream_ = nullptr; // Stream for reading
15101513
detail::BodyReader body_reader_; // Body reading state
15111514

1515+
// Compression support
1516+
std::unique_ptr<detail::decompressor> decompressor_;
1517+
std::string decompress_buffer_; // Buffer for decompressed data
1518+
size_t decompress_offset_ = 0; // Read position in decompress_buffer_
1519+
15121520
// Default constructor
15131521
StreamHandle() = default;
15141522

@@ -1532,35 +1540,25 @@ class ClientImpl {
15321540
bool is_socket_direct_mode() const { return stream_ != nullptr; }
15331541

15341542
// Read up to len bytes into buf, returns number of bytes read (0 at EOF)
1535-
ssize_t read(char *buf, size_t len) {
1536-
if (!is_valid() || !response) { return -1; }
1537-
1538-
if (is_socket_direct_mode()) {
1539-
// Socket direct mode: read from stream via BodyReader
1540-
return body_reader_.read(buf, len);
1541-
} else {
1542-
// Memory buffer mode: read from pre-loaded response body
1543-
const auto &body = response->body;
1544-
if (read_offset_ >= body.size()) { return 0; }
1543+
// Implementation is below decompressor class definition
1544+
ssize_t read(char *buf, size_t len);
15451545

1546-
auto remaining = body.size() - read_offset_;
1547-
auto to_read = (std::min)(len, remaining);
1548-
std::memcpy(buf, body.data() + read_offset_, to_read);
1549-
read_offset_ += to_read;
1550-
return static_cast<ssize_t>(to_read);
1551-
}
1552-
}
1546+
private:
1547+
// Read with decompression support (implemented after decompressor class)
1548+
ssize_t read_with_decompression(char *buf, size_t len);
15531549

1550+
public:
15541551
// Read all remaining content into a string
15551552
std::string read_all() {
15561553
if (!is_valid() || !response) { return {}; }
15571554

15581555
if (is_socket_direct_mode()) {
1559-
// Socket direct mode: read all from stream
1556+
// Socket direct mode: read all from stream (uses read() for
1557+
// decompression)
15601558
std::string result;
15611559
char buf[8192];
15621560
ssize_t n;
1563-
while ((n = body_reader_.read(buf, sizeof(buf))) > 0) {
1561+
while ((n = read(buf, sizeof(buf))) > 0) {
15641562
result.append(buf, static_cast<size_t>(n));
15651563
}
15661564
return result;
@@ -2946,6 +2944,75 @@ inline bool is_field_value(const std::string &s) { return is_field_content(s); }
29462944

29472945
} // namespace detail
29482946

2947+
// ----------------------------------------------------------------------------
2948+
// StreamHandle method implementations (after decompressor class definition)
2949+
// ----------------------------------------------------------------------------
2950+
2951+
inline ssize_t ClientImpl::StreamHandle::read(char *buf, size_t len) {
2952+
if (!is_valid() || !response) { return -1; }
2953+
2954+
if (is_socket_direct_mode()) {
2955+
// Socket direct mode: read from stream via BodyReader
2956+
if (decompressor_) { return read_with_decompression(buf, len); }
2957+
return body_reader_.read(buf, len);
2958+
} else {
2959+
// Memory buffer mode: read from pre-loaded response body
2960+
const auto &body = response->body;
2961+
if (read_offset_ >= body.size()) { return 0; }
2962+
2963+
auto remaining = body.size() - read_offset_;
2964+
auto to_read = (std::min)(len, remaining);
2965+
std::memcpy(buf, body.data() + read_offset_, to_read);
2966+
read_offset_ += to_read;
2967+
return static_cast<ssize_t>(to_read);
2968+
}
2969+
}
2970+
2971+
inline ssize_t ClientImpl::StreamHandle::read_with_decompression(char *buf,
2972+
size_t len) {
2973+
// First, return any buffered decompressed data
2974+
if (decompress_offset_ < decompress_buffer_.size()) {
2975+
auto available = decompress_buffer_.size() - decompress_offset_;
2976+
auto to_copy = (std::min)(len, available);
2977+
std::memcpy(buf, decompress_buffer_.data() + decompress_offset_, to_copy);
2978+
decompress_offset_ += to_copy;
2979+
return static_cast<ssize_t>(to_copy);
2980+
}
2981+
2982+
// Buffer exhausted, read more compressed data and decompress
2983+
decompress_buffer_.clear();
2984+
decompress_offset_ = 0;
2985+
2986+
char compressed_buf[8192];
2987+
auto n = body_reader_.read(compressed_buf, sizeof(compressed_buf));
2988+
2989+
if (n <= 0) { return n; } // EOF or error
2990+
2991+
// Decompress the data
2992+
bool decompress_ok =
2993+
decompressor_->decompress(compressed_buf, static_cast<size_t>(n),
2994+
[this](const char *data, size_t data_len) {
2995+
decompress_buffer_.append(data, data_len);
2996+
return true;
2997+
});
2998+
2999+
if (!decompress_ok) {
3000+
body_reader_.last_error = Error::Read;
3001+
return -1;
3002+
}
3003+
3004+
if (decompress_buffer_.empty()) {
3005+
// Decompressor needs more data, try again
3006+
return read_with_decompression(buf, len);
3007+
}
3008+
3009+
// Return from the newly decompressed buffer
3010+
auto to_copy = (std::min)(len, decompress_buffer_.size());
3011+
std::memcpy(buf, decompress_buffer_.data(), to_copy);
3012+
decompress_offset_ = to_copy;
3013+
return static_cast<ssize_t>(to_copy);
3014+
}
3015+
29493016
// ----------------------------------------------------------------------------
29503017

29513018
/*
@@ -9419,6 +9486,30 @@ ClientImpl::open_stream_direct(const std::string &path,
94199486
handle.response->get_header_value("Transfer-Encoding");
94209487
handle.body_reader_.chunked = (transfer_encoding == "chunked");
94219488

9489+
// Set up decompressor based on Content-Encoding
9490+
auto content_encoding = handle.response->get_header_value("Content-Encoding");
9491+
if (!content_encoding.empty()) {
9492+
#ifdef CPPHTTPLIB_ZLIB_SUPPORT
9493+
if (content_encoding == "gzip" || content_encoding == "deflate") {
9494+
handle.decompressor_ = detail::make_unique<detail::gzip_decompressor>();
9495+
} else
9496+
#endif
9497+
#ifdef CPPHTTPLIB_BROTLI_SUPPORT
9498+
if (content_encoding == "br") {
9499+
handle.decompressor_ = detail::make_unique<detail::brotli_decompressor>();
9500+
} else
9501+
#endif
9502+
#ifdef CPPHTTPLIB_ZSTD_SUPPORT
9503+
if (content_encoding == "zstd") {
9504+
handle.decompressor_ = detail::make_unique<detail::zstd_decompressor>();
9505+
} else
9506+
#endif
9507+
{
9508+
// Unsupported encoding - leave decompressor_ null
9509+
// Data will be returned as-is (compressed)
9510+
}
9511+
}
9512+
94229513
return handle;
94239514
}
94249515

0 commit comments

Comments
 (0)