Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -264,8 +264,8 @@ jobs:
apt-get: >-
${{ matrix.install }}
build-essential
zlib1g-dev libbrotli-dev
${{ matrix.x86 && 'zlib1g-dev:i386 libbrotli-dev:i386' || '' }}
zlib1g-dev libbrotli-dev libzstd-dev
${{ matrix.x86 && 'zlib1g-dev:i386 libbrotli-dev:i386 libzstd-dev:i386' || '' }}

- name: Clone Boost
uses: alandefreitas/cpp-actions/boost-clone@v1.9.3
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/code-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ jobs:
- name: Patch CI script for extra source directories
run: |
# The CI script only symlinks 'include/' and 'src/' at boost-root level,
# but this repo also has src_zlib/ and src_brotli/. After fix_paths.py
# but this repo also has src_zlib/, src_brotli/ and src_zstd/. After fix_paths.py
# strips 'libs/http/', gcovr can't find these files without symlinks.
sed -i '/ln -sfn "\$BOOST_CI_SRC_FOLDER\/src" /a\
for _d in "$BOOST_CI_SRC_FOLDER"/src_*; do [ -d "$_d" ] && ln -sfn "$_d" "$(pwd)/$(basename "$_d")" 2>/dev/null || true; done' \
Expand Down
16 changes: 16 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,22 @@ if (Brotli_FOUND)
target_compile_definitions(boost_http_brotli PRIVATE BOOST_HTTP_SOURCE)
endif ()

# Zstd
find_package(Zstd 1.4.0)
if (Zstd_FOUND)
file(GLOB_RECURSE BOOST_HTTP_ZSTD_HEADERS CONFIGURE_DEPENDS include/boost/http/zstd/*.hpp)
file(GLOB_RECURSE BOOST_HTTP_ZSTD_SOURCES CONFIGURE_DEPENDS src_zstd/*.cpp src_zstd/*.hpp)
source_group("" FILES "include/boost/http/zstd.hpp")
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR}/include/boost/http/zstd PREFIX "include" FILES ${BOOST_HTTP_ZSTD_HEADERS})
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR}/src_zstd PREFIX "src" FILES ${BOOST_HTTP_ZSTD_SOURCES})
add_library(boost_http_zstd include/boost/http/zstd.hpp build/Jamfile ${BOOST_HTTP_ZSTD_HEADERS} ${BOOST_HTTP_ZSTD_SOURCES})
add_library(Boost::http_zstd ALIAS boost_http_zstd)
target_link_libraries(boost_http_zstd PUBLIC boost_http)
target_link_libraries(boost_http_zstd PRIVATE Zstd::Zstd)
target_compile_definitions(boost_http_zstd PUBLIC BOOST_HTTP_HAS_ZSTD)
target_compile_definitions(boost_http_zstd PRIVATE BOOST_HTTP_SOURCE)
endif ()

#-------------------------------------------------
#
# Tests
Expand Down
18 changes: 17 additions & 1 deletion build/Jamfile
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,20 @@ lib boost_http_brotli
<define>BOOST_HTTP_HAS_BROTLI
;

boost-install boost_http boost_http_zlib boost_http_brotli ;
# Zstd
using zstd ;

alias http_zstd_sources : [ glob-tree-ex src_zstd : *.cpp ] ;

lib boost_http_zstd
: http_zstd_sources
: requirements
<library>/boost/http//boost_http
<define>BOOST_HTTP_SOURCE
[ ac.check-library /zstd//zstd : <library>/zstd//zstd : <build>no ]
: usage-requirements
<library>/boost/http//boost_http
<define>BOOST_HTTP_HAS_ZSTD
;

boost-install boost_http boost_http_zlib boost_http_brotli boost_http_zstd ;
44 changes: 44 additions & 0 deletions cmake/FindZstd.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#
# Copyright (c) 2026 Mohammad Nejati
#
# Distributed under the Boost Software License, Version 1.0. (See accompanying
# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
#
# Official repository: https://github.com/cppalliance/http
#

# Provides imported targets:
# Zstd::Zstd

find_path(Zstd_INCLUDE_DIR NAMES "zstd.h")
find_library(Zstd_LIBRARY NAMES zstd libzstd zstd_static)

if(Zstd_INCLUDE_DIR AND EXISTS "${Zstd_INCLUDE_DIR}/zstd.h")
file(STRINGS "${Zstd_INCLUDE_DIR}/zstd.h" Zstd_VERSION_LINES
REGEX "^#define[ \t]+ZSTD_VERSION_(MAJOR|MINOR|RELEASE)[ \t]+[0-9]+")
string(REGEX REPLACE ".*ZSTD_VERSION_MAJOR[ \t]+([0-9]+).*" "\\1" Zstd_VERSION_MAJOR "${Zstd_VERSION_LINES}")
string(REGEX REPLACE ".*ZSTD_VERSION_MINOR[ \t]+([0-9]+).*" "\\1" Zstd_VERSION_MINOR "${Zstd_VERSION_LINES}")
string(REGEX REPLACE ".*ZSTD_VERSION_RELEASE[ \t]+([0-9]+).*" "\\1" Zstd_VERSION_RELEASE "${Zstd_VERSION_LINES}")
set(Zstd_VERSION "${Zstd_VERSION_MAJOR}.${Zstd_VERSION_MINOR}.${Zstd_VERSION_RELEASE}")
unset(Zstd_VERSION_LINES)
endif()

include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(Zstd
REQUIRED_VARS
Zstd_INCLUDE_DIR
Zstd_LIBRARY
VERSION_VAR
Zstd_VERSION
)

if(Zstd_FOUND)
add_library(Zstd::Zstd UNKNOWN IMPORTED)
set_target_properties(Zstd::Zstd PROPERTIES
IMPORTED_LOCATION "${Zstd_LIBRARY}"
INTERFACE_INCLUDE_DIRECTORIES "${Zstd_INCLUDE_DIR}")
endif()

mark_as_advanced(
Zstd_INCLUDE_DIR
Zstd_LIBRARY)
1 change: 1 addition & 0 deletions doc/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
* xref:5.compression/5.compression.adoc[Compression]
** xref:5.compression/5a.zlib.adoc[ZLib]
** xref:5.compression/5b.brotli.adoc[Brotli]
** xref:5.compression/5c.zstd.adoc[Zstandard]
* xref:6.design/6.design.adoc[Design]
** xref:6.design/6a.sans-io.adoc[Sans-I/O Philosophy]
** xref:6.design/6b.parser.adoc[Parser]
Expand Down
1 change: 1 addition & 0 deletions doc/modules/ROOT/pages/5.compression/5a.zlib.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -214,3 +214,4 @@ ser_cfg.apply_gzip_encoder = true;
== See Also

* xref:5.compression/5b.brotli.adoc[Brotli] — Higher compression ratio
* xref:5.compression/5c.zstd.adoc[Zstandard] — Fast compression with a wide range of levels
1 change: 1 addition & 0 deletions doc/modules/ROOT/pages/5.compression/5b.brotli.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,4 @@ ser_cfg.apply_brotli_encoder = true;
== See Also

* xref:5.compression/5a.zlib.adoc[ZLib] — DEFLATE/gzip compression
* xref:5.compression/5c.zstd.adoc[Zstandard] — Fast compression with a wide range of levels
263 changes: 263 additions & 0 deletions doc/modules/ROOT/pages/5.compression/5c.zstd.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
= Zstandard Compression
:navtitle: Zstandard

The Zstandard module provides fast compression and decompression services for HTTP content encoding.

== Overview

Zstandard (zstd) is a lossless compression algorithm designed for real-time scenarios. It offers a very wide range of speed/ratio trade-offs through its compression levels, compresses at zlib-like speeds with better ratios, and decompresses quickly regardless of the level used. It is registered as the `zstd` HTTP content coding (RFC 8878).

The services are thin wrappers over the stable `libzstd` API. The `Boost::http_zstd` library is built when zstd 1.4.0 or later is found, and defines `BOOST_HTTP_HAS_ZSTD` for its consumers.

== Basic Usage

[source,cpp]
----
#include <boost/http/zstd.hpp>

namespace zstd = boost::http::zstd;

// Install services into an execution context
auto& compressor = zstd::install_compress_service(ctx);
auto& decompressor = zstd::install_decompress_service(ctx);

// Or install both into the system context
zstd::install_zstd_service();
----

=== Results and Errors

Most functions return a `std::size_t` which is either a byte count or an
encoded error code, exactly like the underlying C API. Always test a result
with `is_error` before using it:

[source,cpp]
----
std::size_t n = compressor.compress(/* ... */);
if (compressor.is_error(n))
{
boost::system::error_code ec = compressor.get_error_code(n);
std::cerr << compressor.get_error_name(n) << '\n';
return ec;
}
----

`zstd::error` is a Boost.System error enum, so an error code converts to
`boost::system::error_code` and `std::error_code`.

=== One-Shot Compression

[source,cpp]
----
std::string input = /* ... */;
std::string output(compressor.compress_bound(input.size()), '\0');

std::size_t n = compressor.compress(
output.data(), output.size(),
input.data(), input.size(),
compressor.default_level());

if (! compressor.is_error(n))
output.resize(n);
----

=== Compression Levels

Levels range from `min_level()` (negative, fastest) to `max_level()`
(slowest, best ratio). `default_level()` returns the library default:

[source,cpp]
----
int fastest = compressor.min_level(); // negative "fast" levels
int best = compressor.max_level(); // 22
int normal = compressor.default_level(); // 3
----

=== One-Shot Decompression

The frame header usually records the content size, which gives the exact
output buffer size:

[source,cpp]
----
auto size = decompressor.get_frame_content_size(
compressed.data(), compressed.size());

if (size == zstd::content_size_error)
return; // not a zstd frame
if (size == zstd::content_size_unknown)
return; // must use the streaming interface

std::string output(size, '\0');
std::size_t n = decompressor.decompress(
output.data(), output.size(),
compressed.data(), compressed.size());
----

The recorded size comes from the peer; check it against an application
limit before allocating.

== Streaming Interface

Contexts hold the state of a frame in progress. Input and output are
described by `in_buffer` and `out_buffer`, whose `pos` fields the service
advances.

=== Compression

[source,cpp]
----
zstd::cctx* ctx = compressor.create_cctx();
compressor.set_parameter(ctx, zstd::c_parameter::compression_level, 5);
compressor.set_parameter(ctx, zstd::c_parameter::checksum_flag, 1);

std::vector<char> buf(compressor.stream_out_size());
zstd::in_buffer in{ input.data(), input.size(), 0 };
std::size_t remaining;
do
{
zstd::out_buffer out{ buf.data(), buf.size(), 0 };
remaining = compressor.compress_stream(
ctx, out, in, zstd::end_directive::end);
if (compressor.is_error(remaining))
break;
output.insert(output.end(), buf.data(), buf.data() + out.pos);
}
while (remaining != 0);

compressor.free_cctx(ctx);
----

Use `end_directive::continue_` while more input is coming, `flush` to force
out a decodable block without closing the frame, and `end` to finish the
frame. With `flush` and `end`, keep calling until zero is returned.

A context is reusable: `reset` with `reset_directive::session_only` starts
another frame with the same parameters.

=== Decompression

[source,cpp]
----
zstd::dctx* ctx = decompressor.create_dctx();

std::vector<char> buf(decompressor.stream_out_size());
zstd::in_buffer in{ compressed.data(), compressed.size(), 0 };
std::size_t rs;
do
{
zstd::out_buffer out{ buf.data(), buf.size(), 0 };
rs = decompressor.decompress_stream(ctx, out, in);
if (decompressor.is_error(rs))
break;
output.insert(output.end(), buf.data(), buf.data() + out.pos);
}
while (rs != 0);

decompressor.free_dctx(ctx);
----

`decompress_stream` returns zero when a frame is complete and fully flushed.
Any other non-error value means more input or more output space is needed.

=== Parameters

Parameters are set on a context and are "sticky": they apply to every frame
processed with that context until it is reset with
`reset_directive::parameters`. Valid ranges can be queried:

[source,cpp]
----
zstd::bounds b = compressor.param_bounds(zstd::c_parameter::window_log);
if (! compressor.is_error(b.error))
std::cout << b.lower_bound << ".." << b.upper_bound;
----

|===
| Parameter | Description

| `c_parameter::compression_level`
| Compression level; negative values select faster modes

| `c_parameter::window_log`
| Maximum back-reference distance as a power of 2; bounds decoder memory

| `c_parameter::strategy`
| Match-finding strategy, see `zstd::strategy`

| `c_parameter::checksum_flag`
| Append a 32-bit content checksum to the frame

| `c_parameter::content_size_flag`
| Record the content size in the frame header when known

| `d_parameter::window_log_max`
| Largest window the decoder will allocate for in streaming mode
|===

== Dictionaries

Dictionaries improve compression of many small, similar messages. A digested
dictionary (`cdict` / `ddict`) is prepared once and shared read-only between
contexts and threads:

[source,cpp]
----
zstd::cdict* cd = compressor.create_cdict(dict.data(), dict.size(), 3);
compressor.ref_cdict(ctx, cd); // used by all following frames
// ... compress ...
compressor.free_cdict(cd); // after the context stops using it

zstd::ddict* dd = decompressor.create_ddict(dict.data(), dict.size());
decompressor.ref_ddict(ctx, dd);
----

`load_dictionary` copies and digests a dictionary into a single context, and
`ref_prefix` references raw content as a single-use dictionary for the next
frame only.

== Reference

=== Functions

|===
| Function | Description

| `zstd::install_compress_service`
| Install compression service into an execution context

| `zstd::install_decompress_service`
| Install decompression service into an execution context

| `zstd::install_zstd_service`
| Install both services into the system context
|===

=== Types

|===
| Type | Description

| `zstd::compress_service`
| Compression service interface

| `zstd::decompress_service`
| Decompression service interface

| `zstd::cctx`, `zstd::dctx`
| Opaque compression and decompression contexts

| `zstd::cdict`, `zstd::ddict`
| Opaque digested dictionaries

| `zstd::in_buffer`, `zstd::out_buffer`
| Streaming buffer descriptors

| `zstd::error`
| Error codes
|===

== See Also

* xref:5.compression/5a.zlib.adoc[ZLib] — DEFLATE/gzip compression
* xref:5.compression/5b.brotli.adoc[Brotli] — Higher compression ratio
Loading
Loading