From 4b8f4e92d1d64611a373b288ff23a4d971c8af51 Mon Sep 17 00:00:00 2001 From: Mohammad Nejati Date: Tue, 25 Aug 2026 09:16:22 +0330 Subject: [PATCH] Add zstd services --- .github/workflows/ci.yml | 4 +- .github/workflows/code-coverage.yml | 2 +- CMakeLists.txt | 16 + build/Jamfile | 18 +- cmake/FindZstd.cmake | 44 ++ doc/modules/ROOT/nav.adoc | 1 + .../ROOT/pages/5.compression/5a.zlib.adoc | 1 + .../ROOT/pages/5.compression/5b.brotli.adoc | 1 + .../ROOT/pages/5.compression/5c.zstd.adoc | 263 ++++++++ .../ROOT/pages/7.reference/7.reference.adoc | 50 ++ doc/outline.md | 6 + include/boost/http/zstd.hpp | 45 ++ include/boost/http/zstd/compress.hpp | 564 ++++++++++++++++++ include/boost/http/zstd/decompress.hpp | 461 ++++++++++++++ include/boost/http/zstd/error.hpp | 79 +++ include/boost/http/zstd/impl/error.hpp | 84 +++ include/boost/http/zstd/service.hpp | 62 ++ include/boost/http/zstd/types.hpp | 138 +++++ src/zstd/error.cpp | 102 ++++ src_zstd/compress.cpp | 312 ++++++++++ src_zstd/decompress.cpp | 286 +++++++++ test/unit/CMakeLists.txt | 4 + test/unit/Jamfile | 1 + test/unit/zstd.cpp | 488 +++++++++++++++ test/unit/zstd/compress.cpp | 11 + test/unit/zstd/decompress.cpp | 11 + test/unit/zstd/zstd_error.cpp | 11 + test/unit/zstd/zstd_types.cpp | 11 + 28 files changed, 3072 insertions(+), 4 deletions(-) create mode 100644 cmake/FindZstd.cmake create mode 100644 doc/modules/ROOT/pages/5.compression/5c.zstd.adoc create mode 100644 include/boost/http/zstd.hpp create mode 100644 include/boost/http/zstd/compress.hpp create mode 100644 include/boost/http/zstd/decompress.hpp create mode 100644 include/boost/http/zstd/error.hpp create mode 100644 include/boost/http/zstd/impl/error.hpp create mode 100644 include/boost/http/zstd/service.hpp create mode 100644 include/boost/http/zstd/types.hpp create mode 100644 src/zstd/error.cpp create mode 100644 src_zstd/compress.cpp create mode 100644 src_zstd/decompress.cpp create mode 100644 test/unit/zstd.cpp create mode 100644 test/unit/zstd/compress.cpp create mode 100644 test/unit/zstd/decompress.cpp create mode 100644 test/unit/zstd/zstd_error.cpp create mode 100644 test/unit/zstd/zstd_types.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a0f66c76..e1fd56fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index b6b0bc83..590c847e 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -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' \ diff --git a/CMakeLists.txt b/CMakeLists.txt index ffcfffcc..98716fa8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/build/Jamfile b/build/Jamfile index 2d42c923..dc22139c 100644 --- a/build/Jamfile +++ b/build/Jamfile @@ -95,4 +95,20 @@ lib boost_http_brotli 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 + /boost/http//boost_http + BOOST_HTTP_SOURCE + [ ac.check-library /zstd//zstd : /zstd//zstd : no ] + : usage-requirements + /boost/http//boost_http + BOOST_HTTP_HAS_ZSTD + ; + +boost-install boost_http boost_http_zlib boost_http_brotli boost_http_zstd ; diff --git a/cmake/FindZstd.cmake b/cmake/FindZstd.cmake new file mode 100644 index 00000000..575691e0 --- /dev/null +++ b/cmake/FindZstd.cmake @@ -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) diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc index 11ff5332..c726f4bd 100644 --- a/doc/modules/ROOT/nav.adoc +++ b/doc/modules/ROOT/nav.adoc @@ -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] diff --git a/doc/modules/ROOT/pages/5.compression/5a.zlib.adoc b/doc/modules/ROOT/pages/5.compression/5a.zlib.adoc index d2fe0be3..557a79d2 100644 --- a/doc/modules/ROOT/pages/5.compression/5a.zlib.adoc +++ b/doc/modules/ROOT/pages/5.compression/5a.zlib.adoc @@ -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 diff --git a/doc/modules/ROOT/pages/5.compression/5b.brotli.adoc b/doc/modules/ROOT/pages/5.compression/5b.brotli.adoc index 2ecf5f51..770eb6b2 100644 --- a/doc/modules/ROOT/pages/5.compression/5b.brotli.adoc +++ b/doc/modules/ROOT/pages/5.compression/5b.brotli.adoc @@ -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 diff --git a/doc/modules/ROOT/pages/5.compression/5c.zstd.adoc b/doc/modules/ROOT/pages/5.compression/5c.zstd.adoc new file mode 100644 index 00000000..06c0181e --- /dev/null +++ b/doc/modules/ROOT/pages/5.compression/5c.zstd.adoc @@ -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 + +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 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 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 diff --git a/doc/modules/ROOT/pages/7.reference/7.reference.adoc b/doc/modules/ROOT/pages/7.reference/7.reference.adoc index c39b9d6c..34ca2348 100644 --- a/doc/modules/ROOT/pages/7.reference/7.reference.adoc +++ b/doc/modules/ROOT/pages/7.reference/7.reference.adoc @@ -317,6 +317,56 @@ cpp:boost::http::brotli::error[error] |=== +[width=100%] +|=== +2+| *Zstandard* + +| **Types** + +cpp:boost::http::zstd::bounds[bounds] + +cpp:boost::http::zstd::cctx[cctx] + +cpp:boost::http::zstd::cdict[cdict] + +cpp:boost::http::zstd::compress_service[compress_service] + +cpp:boost::http::zstd::dctx[dctx] + +cpp:boost::http::zstd::ddict[ddict] + +cpp:boost::http::zstd::decompress_service[decompress_service] + +cpp:boost::http::zstd::in_buffer[in_buffer] + +cpp:boost::http::zstd::out_buffer[out_buffer] + +**Functions** + +cpp:boost::http::zstd::install_compress_service[install_compress_service] + +cpp:boost::http::zstd::install_decompress_service[install_decompress_service] + +cpp:boost::http::zstd::install_zstd_service[install_zstd_service] + +| **Constants** + +cpp:boost::http::zstd::c_parameter[c_parameter] + +cpp:boost::http::zstd::content_size[content_size] + +cpp:boost::http::zstd::d_parameter[d_parameter] + +cpp:boost::http::zstd::end_directive[end_directive] + +cpp:boost::http::zstd::error[error] + +cpp:boost::http::zstd::reset_directive[reset_directive] + +cpp:boost::http::zstd::strategy[strategy] + +|=== + [width=100%] |=== 2+| *bcrypt* | *JSON* diff --git a/doc/outline.md b/doc/outline.md index e4ac4526..2e2cb4f1 100644 --- a/doc/outline.md +++ b/doc/outline.md @@ -149,6 +149,12 @@ Renamed from: `compression/zlib.adoc` Renamed from: `compression/brotli.adoc` +### 5c. 5c.zstd.adoc — Zstandard + +- Fast compression with a wide range of levels, dictionaries + +New page (no existing equivalent) + ## 6. Design ### 6a. 6a.sans-io.adoc — Sans-I/O Philosophy diff --git a/include/boost/http/zstd.hpp b/include/boost/http/zstd.hpp new file mode 100644 index 00000000..689d2c29 --- /dev/null +++ b/include/boost/http/zstd.hpp @@ -0,0 +1,45 @@ +// +// 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 +// + +/** @file + Zstandard compression and decompression library. + + This header includes all Zstandard-related functionality including + compression, decompression, dictionary support, and error handling. + + Zstandard (zstd) is a fast lossless compression algorithm targeting + real-time compression scenarios. It offers a very wide range of + compression / speed trade-offs through its compression levels, + compresses at zlib-like speeds with better ratios, and is backed by + a very fast decoder whose speed does not depend on the level used. + + @code + #include + #include + + // Create a datastore for services + boost::http::datastore ctx; + + // Install compression and decompression services + auto& compressor = boost::http::zstd::install_compress_service(ctx); + auto& decompressor = boost::http::zstd::install_decompress_service(ctx); + @endcode +*/ + +#ifndef BOOST_HTTP_ZSTD_HPP +#define BOOST_HTTP_ZSTD_HPP + +#include +#include +#include +#include +#include +#include + +#endif diff --git a/include/boost/http/zstd/compress.hpp b/include/boost/http/zstd/compress.hpp new file mode 100644 index 00000000..3906fd34 --- /dev/null +++ b/include/boost/http/zstd/compress.hpp @@ -0,0 +1,564 @@ +// +// 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 +// + +#ifndef BOOST_HTTP_ZSTD_COMPRESS_HPP +#define BOOST_HTTP_ZSTD_COMPRESS_HPP + +#include +#include +#include +#include + +#include + +#include + +namespace boost { +namespace http { +namespace zstd { + +/** Opaque structure that holds compression context state. + + A context is created with @ref compress_service::create_cctx + and released with @ref compress_service::free_cctx. It holds + the sticky parameters and the state of the frame being + compressed, and may be reused for successive frames. +*/ +struct cctx; + +/** Opaque structure that holds a digested compression dictionary. + + Created with @ref compress_service::create_cdict and released + with @ref compress_service::free_cdict. A digested dictionary + is read-only and may be shared by multiple contexts and threads. +*/ +struct cdict; + +/** Streaming end directives. + + These values control how @ref compress_service::compress_stream + treats the input it is given. +*/ +enum class end_directive +{ + /** Collect more data; the encoder decides when to emit output. */ + continue_ = 0, + + /** Flush all data provided so far. + + Creates at least one new block that can be decoded + immediately on reception. The frame continues, so + future data can still reference previous content. + */ + flush = 1, + + /** Flush all remaining data and close the current frame. */ + end = 2 +}; + +/** Compression strategies, listed from fastest to strongest. + + Selected with the @ref c_parameter::strategy parameter. + New strategies may be added in the future; only the + ordering from fast to strong is guaranteed. +*/ +enum class strategy +{ + fast = 1, + dfast = 2, + greedy = 3, + lazy = 4, + lazy2 = 5, + btlazy2 = 6, + btopt = 7, + btultra = 8, + btultra2 = 9 +}; + +/** Compression parameter identifiers. + + These values identify parameters that can be set on a + compression context with @ref compress_service::set_parameter. + Parameters are sticky: once set they apply to every frame + compressed with that context until the context's parameters + are reset. For the bounded parameters, a value of zero + means "use the default". +*/ +enum class c_parameter +{ + /** Compression level; negative values select faster modes. */ + compression_level = 100, + + /** Maximum back-reference distance, as a power of 2. + + This sets the memory budget for streaming decompression, + with larger values requiring more memory and typically + compressing better. + */ + window_log = 101, + + /** Size of the initial probe table, as a power of 2. */ + hash_log = 102, + + /** Size of the multi-probe search table, as a power of 2. */ + chain_log = 103, + + /** Number of search attempts, as a power of 2. */ + search_log = 104, + + /** Minimum size of searched matches. */ + min_match = 105, + + /** Match length considered "good enough" to stop searching. */ + target_length = 106, + + /** Compression strategy, see @ref boost::http::zstd::strategy. */ + strategy = 107, + + /** Enable long distance matching for large inputs. */ + enable_long_distance_matching = 160, + + /** Size of the long distance matching table, as a power of 2. */ + ldm_hash_log = 161, + + /** Minimum match size for the long distance matcher. */ + ldm_min_match = 162, + + /** Log size of each bucket in the long distance matching table. */ + ldm_bucket_size_log = 163, + + /** Frequency of inserting entries into the long distance matching table. */ + ldm_hash_rate_log = 164, + + /** Write the content size into the frame header whenever known (default: 1). */ + content_size_flag = 200, + + /** Write a 32-bit checksum of the content at the end of the frame (default: 0). */ + checksum_flag = 201, + + /** Write the dictionary ID into the frame header when applicable (default: 1). */ + dict_id_flag = 202, + + /** Number of worker threads; zero selects single-threaded mode. */ + nb_workers = 400, + + /** Size of a compression job when using worker threads. */ + job_size = 401, + + /** Overlap between jobs, as a fraction of the window size (0-9). */ + overlap_log = 402 +}; + +/** Provides the Zstandard compression API. + + This service interface exposes Zstandard compression + functionality through a set of virtual functions. Data + can be compressed in one shot with @ref compress or + @ref compress2, or incrementally with @ref compress_stream. + + Most functions return a `std::size_t` which is either a + byte count or an encoded error code. Test results with + @ref is_error and convert them with @ref get_error_code + or @ref get_error_name. + + Compression contexts are reusable: after a frame is + complete, the same context can compress another frame, + keeping the parameters that were set on it. + + @code + // Example: Simple one-shot compression + auto& compressor = boost::http::zstd::install_compress_service(ctx); + + std::vector input = get_input(); + std::vector output(compressor.compress_bound(input.size())); + + 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); + // Use compressed data + } + @endcode + + @code + // Example: Streaming compression + auto* ctx = compressor.create_cctx(); + + compressor.set_parameter(ctx, + boost::http::zstd::c_parameter::compression_level, 5); + compressor.set_parameter(ctx, + boost::http::zstd::c_parameter::checksum_flag, 1); + + std::vector buf(compressor.stream_out_size()); + boost::http::zstd::in_buffer in{ input.data(), input.size(), 0 }; + std::size_t remaining; + do + { + boost::http::zstd::out_buffer out{ buf.data(), buf.size(), 0 }; + remaining = compressor.compress_stream(ctx, out, in, + boost::http::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); + @endcode +*/ +struct BOOST_SYMBOL_VISIBLE + compress_service + : capy::execution_context::service +{ + /** Return the Zstandard library version number. + @return The version as `MAJOR * 10000 + MINOR * 100 + RELEASE`. + */ + virtual + unsigned + version_number() const noexcept = 0; + + /** Return the Zstandard library version string. + @return Pointer to a string such as "1.5.7". + */ + virtual + char const* + version_string() const noexcept = 0; + + /** Return the minimum compression level. + @return The most negative level allowed. + */ + virtual + int + min_level() const noexcept = 0; + + /** Return the maximum compression level. + @return The highest level available. + */ + virtual + int + max_level() const noexcept = 0; + + /** Return the default compression level. + @return The level used when none is specified. + */ + virtual + int + default_level() const noexcept = 0; + + /** Return the maximum compressed size in the worst case. + @param src_size The size of the input data. + @return An upper bound on the compressed size of a + single frame, or an error code if `src_size` + is too large. + */ + virtual + std::size_t + compress_bound(std::size_t src_size) const noexcept = 0; + + /** Compress data in one call as a single frame. + @param dst Output buffer. + @param dst_capacity Output buffer size. + @param src Input data. + @param src_size Input data size. + @param level The compression level. + @return The compressed size, or an error code. + */ + virtual + std::size_t + compress( + void* dst, + std::size_t dst_capacity, + void const* src, + std::size_t src_size, + int level) const noexcept = 0; + + /** Create a new compression context. + @return Pointer to the context, or nullptr on error. + */ + virtual + cctx* + create_cctx() const noexcept = 0; + + /** Release a compression context. + @param ctx The context to release; may be nullptr. + @return Zero, or an error code. + */ + virtual + std::size_t + free_cctx(cctx* ctx) const noexcept = 0; + + /** Return the current memory usage of a compression context. + @param ctx The context. + @return Memory usage in bytes. + */ + virtual + std::size_t + sizeof_cctx(cctx const* ctx) const noexcept = 0; + + /** Return the valid bounds of a compression parameter. + @param param The parameter identifier. + @return The bounds; test the `error` field with @ref is_error. + */ + virtual + bounds + param_bounds(c_parameter param) const noexcept = 0; + + /** Set a compression parameter. + + Parameters can only be set between frames, before + compression of the next frame starts. Values beyond + the bounds are either clamped or rejected, depending + on the parameter. + + @param ctx The context. + @param param The parameter identifier. + @param value The parameter value. + @return Zero, or an error code. + */ + virtual + std::size_t + set_parameter( + cctx* ctx, + c_parameter param, + int value) const noexcept = 0; + + /** Declare the total input size of the next frame. + + The value is written into the frame header and checked + at the end of the frame. It applies to the next frame + only; afterwards the size reverts to unknown. + + @param ctx The context. + @param pledged_src_size The input size, or + @ref content_size_unknown. + @return Zero, or an error code. + */ + virtual + std::size_t + set_pledged_src_size( + cctx* ctx, + unsigned long long pledged_src_size) const noexcept = 0; + + /** Reset a compression context. + @param ctx The context. + @param directive What to reset. + @return Zero, or an error code. + */ + virtual + std::size_t + reset( + cctx* ctx, + reset_directive directive) const noexcept = 0; + + /** Compress data in one call using a context's parameters. + + Always starts a new frame; any unfinished frame held + by the context is discarded. + + @param ctx The context. + @param dst Output buffer. + @param dst_capacity Output buffer size. + @param src Input data. + @param src_size Input data size. + @return The compressed size, or an error code. + */ + virtual + std::size_t + compress2( + cctx* ctx, + void* dst, + std::size_t dst_capacity, + void const* src, + std::size_t src_size) const noexcept = 0; + + /** Compress data in streaming mode. + + Consumes input from `input` and writes output to + `output`, advancing the `pos` field of each. Input + may not be fully consumed if the output buffer fills + up; present the remaining input again after making + room for more output. + + @param ctx The context. + @param output The output buffer. + @param input The input buffer. + @param end_op The end directive. + @return A minimum estimate of the bytes still buffered + internally, or an error code. With + @ref end_directive::flush or @ref end_directive::end, + keep calling with the same directive until zero + is returned. + */ + virtual + std::size_t + compress_stream( + cctx* ctx, + out_buffer& output, + in_buffer& input, + end_directive end_op) const noexcept = 0; + + /** Return the recommended input buffer size for streaming. + @return Size in bytes. + */ + virtual + std::size_t + stream_in_size() const noexcept = 0; + + /** Return the recommended output buffer size for streaming. + + An output buffer of this size is guaranteed to be able + to flush at least one complete compressed block. + + @return Size in bytes. + */ + virtual + std::size_t + stream_out_size() const noexcept = 0; + + /** Create a digested dictionary for compression. + @param dict The dictionary content; copied internally. + @param dict_size The dictionary size. + @param level The compression level to digest for. + @return Pointer to the dictionary, or nullptr on error. + */ + virtual + cdict* + create_cdict( + void const* dict, + std::size_t dict_size, + int level) const noexcept = 0; + + /** Release a digested dictionary. + @param dict The dictionary to release; may be nullptr. + @return Zero, or an error code. + */ + virtual + std::size_t + free_cdict(cdict* dict) const noexcept = 0; + + /** Return the current memory usage of a digested dictionary. + @param dict The dictionary. + @return Memory usage in bytes. + */ + virtual + std::size_t + sizeof_cdict(cdict const* dict) const noexcept = 0; + + /** Load a dictionary into a context. + + The content is copied and digested; it is used for all + future frames until the parameters are reset or another + dictionary is loaded. Loading a null or empty dictionary + returns to no-dictionary mode. + + @param ctx The context. + @param dict The dictionary content. + @param dict_size The dictionary size. + @return Zero, or an error code. + */ + virtual + std::size_t + load_dictionary( + cctx* ctx, + void const* dict, + std::size_t dict_size) const noexcept = 0; + + /** Reference a digested dictionary from a context. + + The dictionary is only referenced and must outlive its + use by the context. Its compression parameters supersede + those set on the context. Referencing nullptr returns + to no-dictionary mode. + + @param ctx The context. + @param dict The digested dictionary. + @return Zero, or an error code. + */ + virtual + std::size_t + ref_cdict( + cctx* ctx, + cdict const* dict) const noexcept = 0; + + /** Reference a prefix for the next frame. + + A prefix is a single-use dictionary of raw content, + discarded once the frame ends. The buffer is only + referenced and must remain valid and unmodified while + the frame is compressed. + + @param ctx The context. + @param prefix The prefix content. + @param prefix_size The prefix size. + @return Zero, or an error code. + */ + virtual + std::size_t + ref_prefix( + cctx* ctx, + void const* prefix, + std::size_t prefix_size) const noexcept = 0; + + /** Return the dictionary ID stored within a dictionary. + @param dict The dictionary content. + @param dict_size The dictionary size. + @return The dictionary ID, or zero if the content is + not a conformant dictionary. + */ + virtual + unsigned + get_dict_id_from_dict( + void const* dict, + std::size_t dict_size) const noexcept = 0; + + /** Check whether a result is an error code. + @param result A value returned from a function of this service. + @return True if the result encodes an error. + */ + virtual + bool + is_error(std::size_t result) const noexcept = 0; + + /** Convert a result to an error code. + @param result A value returned from a function of this service. + @return The error code, or @ref error::no_error. + */ + virtual + error + get_error_code(std::size_t result) const noexcept = 0; + + /** Return a readable description of a result. + @param result A value returned from a function of this service. + @return Pointer to a description string. + */ + virtual + char const* + get_error_name(std::size_t result) const noexcept = 0; + + /** Return a string description of an error code. + @param c The error code. + @return Pointer to error description string. + */ + virtual + char const* + error_string(error c) const noexcept = 0; + +protected: + void shutdown() override {} +}; + +} // zstd +} // http +} // boost + +#endif diff --git a/include/boost/http/zstd/decompress.hpp b/include/boost/http/zstd/decompress.hpp new file mode 100644 index 00000000..4ca7fd14 --- /dev/null +++ b/include/boost/http/zstd/decompress.hpp @@ -0,0 +1,461 @@ +// +// 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 +// + +#ifndef BOOST_HTTP_ZSTD_DECOMPRESS_HPP +#define BOOST_HTTP_ZSTD_DECOMPRESS_HPP + +#include +#include +#include +#include + +#include + +#include + +namespace boost { +namespace http { +namespace zstd { + +/** Opaque structure that holds decompression context state. + + A context is created with @ref decompress_service::create_dctx + and released with @ref decompress_service::free_dctx. It holds + the sticky parameters and the state of the frame being + decompressed, and may be reused for successive frames. +*/ +struct dctx; + +/** Opaque structure that holds a digested decompression dictionary. + + Created with @ref decompress_service::create_ddict and released + with @ref decompress_service::free_ddict. A digested dictionary + is read-only and may be shared by multiple contexts and threads. +*/ +struct ddict; + +/** Decompression parameter identifiers. + + These values identify parameters that can be set on a + decompression context with @ref decompress_service::set_parameter. + Parameters are sticky and remain valid for all following frames. +*/ +enum class d_parameter +{ + /** Maximum window size accepted, as a power of 2. + + In streaming mode the decoder refuses to allocate a + buffer larger than this, protecting the host from + unreasonable memory requirements. Zero selects the + default limit. + */ + window_log_max = 100 +}; + +/** Provides the Zstandard decompression API. + + This service interface exposes Zstandard decompression + functionality through a set of virtual functions. Data + can be decompressed in one shot with @ref decompress or + @ref decompress_dctx when the content size is known, or + incrementally with @ref decompress_stream. + + Most functions return a `std::size_t` which is either a + byte count or an encoded error code. Test results with + @ref is_error and convert them with @ref get_error_code + or @ref get_error_name. + + @code + // Example: Simple one-shot decompression + auto& decompressor = boost::http::zstd::install_decompress_service(ctx); + + std::vector compressed = get_compressed_data(); + auto size = decompressor.get_frame_content_size( + compressed.data(), compressed.size()); + if (size == boost::http::zstd::content_size_error || + size == boost::http::zstd::content_size_unknown) + return; // invalid frame, or streaming mode is required + + std::vector output(size); + std::size_t n = decompressor.decompress( + output.data(), output.size(), + compressed.data(), compressed.size()); + + if (! decompressor.is_error(n)) + { + // Use decompressed data + } + @endcode + + @code + // Example: Streaming decompression + auto* ctx = decompressor.create_dctx(); + + std::vector buf(decompressor.stream_out_size()); + boost::http::zstd::in_buffer in{ compressed.data(), compressed.size(), 0 }; + std::size_t rs; + do + { + boost::http::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); + @endcode +*/ +struct BOOST_SYMBOL_VISIBLE + decompress_service + : capy::execution_context::service +{ + /** Return the Zstandard library version number. + @return The version as `MAJOR * 10000 + MINOR * 100 + RELEASE`. + */ + virtual + unsigned + version_number() const noexcept = 0; + + /** Return the Zstandard library version string. + @return Pointer to a string such as "1.5.7". + */ + virtual + char const* + version_string() const noexcept = 0; + + /** Decompress data in one call. + + The input must be the exact size of one or more + complete frames; the output is their concatenation. + + @param dst Output buffer. + @param dst_capacity Output buffer size; an upper bound + of the decompressed size. + @param src Compressed data. + @param compressed_size Compressed data size. + @return The decompressed size, or an error code. + */ + virtual + std::size_t + decompress( + void* dst, + std::size_t dst_capacity, + void const* src, + std::size_t compressed_size) const noexcept = 0; + + /** Return the decompressed size recorded in a frame header. + + The size is an optional field which is always present + for frames produced by the one-shot functions, and may + be absent for frames produced in streaming mode. If the + source is untrusted the value may be wrong; always check + it against an application limit. + + @param src Start of a frame. + @param src_size Number of bytes available; must cover + the frame header. + @return The content size, @ref content_size_unknown if + it is not recorded, or @ref content_size_error + if the header is invalid or incomplete. + */ + virtual + unsigned long long + get_frame_content_size( + void const* src, + std::size_t src_size) const noexcept = 0; + + /** Return the compressed size of the first frame. + + This may need to scan the whole frame to find its end. + + @param src Start of a frame or skippable frame. + @param src_size Number of bytes available; must cover + the whole first frame. + @return The compressed size of the first frame, or an + error code. + */ + virtual + std::size_t + find_frame_compressed_size( + void const* src, + std::size_t src_size) const noexcept = 0; + + /** Create a new decompression context. + @return Pointer to the context, or nullptr on error. + */ + virtual + dctx* + create_dctx() const noexcept = 0; + + /** Release a decompression context. + @param ctx The context to release; may be nullptr. + @return Zero, or an error code. + */ + virtual + std::size_t + free_dctx(dctx* ctx) const noexcept = 0; + + /** Return the current memory usage of a decompression context. + @param ctx The context. + @return Memory usage in bytes. + */ + virtual + std::size_t + sizeof_dctx(dctx const* ctx) const noexcept = 0; + + /** Return the valid bounds of a decompression parameter. + @param param The parameter identifier. + @return The bounds; test the `error` field with @ref is_error. + */ + virtual + bounds + param_bounds(d_parameter param) const noexcept = 0; + + /** Set a decompression parameter. + + Parameters can only be set between frames, before + decompression of the next frame starts. + + @param ctx The context. + @param param The parameter identifier. + @param value The parameter value. + @return Zero, or an error code. + */ + virtual + std::size_t + set_parameter( + dctx* ctx, + d_parameter param, + int value) const noexcept = 0; + + /** Reset a decompression context. + @param ctx The context. + @param directive What to reset. + @return Zero, or an error code. + */ + virtual + std::size_t + reset( + dctx* ctx, + reset_directive directive) const noexcept = 0; + + /** Decompress data in one call using a context. + + Behaves like @ref decompress, honoring the parameters + and dictionary set on the context. + + @param ctx The context. + @param dst Output buffer. + @param dst_capacity Output buffer size. + @param src Compressed data. + @param src_size Compressed data size. + @return The decompressed size, or an error code. + */ + virtual + std::size_t + decompress_dctx( + dctx* ctx, + void* dst, + std::size_t dst_capacity, + void const* src, + std::size_t src_size) const noexcept = 0; + + /** Decompress data in streaming mode. + + Consumes input from `input` and writes output to + `output`, advancing the `pos` field of each. If + `input.pos < input.size` afterwards, the remaining + input must be presented again. If the output buffer + was filled completely, data may still be buffered + internally; call again to flush it. + + @param ctx The context. + @param output The output buffer. + @param input The input buffer. + @return Zero when a frame is completely decoded and + fully flushed, an error code, or any other + value which means more decoding or flushing + is needed to complete the frame. The value + is a hint for the next input size. + */ + virtual + std::size_t + decompress_stream( + dctx* ctx, + out_buffer& output, + in_buffer& input) const noexcept = 0; + + /** Return the recommended input buffer size for streaming. + @return Size in bytes. + */ + virtual + std::size_t + stream_in_size() const noexcept = 0; + + /** Return the recommended output buffer size for streaming. + + An output buffer of this size is guaranteed to be able + to flush at least one complete block in all circumstances. + + @return Size in bytes. + */ + virtual + std::size_t + stream_out_size() const noexcept = 0; + + /** Create a digested dictionary for decompression. + @param dict The dictionary content; copied internally. + @param dict_size The dictionary size. + @return Pointer to the dictionary, or nullptr on error. + */ + virtual + ddict* + create_ddict( + void const* dict, + std::size_t dict_size) const noexcept = 0; + + /** Release a digested dictionary. + @param dict The dictionary to release; may be nullptr. + @return Zero, or an error code. + */ + virtual + std::size_t + free_ddict(ddict* dict) const noexcept = 0; + + /** Return the current memory usage of a digested dictionary. + @param dict The dictionary. + @return Memory usage in bytes. + */ + virtual + std::size_t + sizeof_ddict(ddict const* dict) const noexcept = 0; + + /** Load a dictionary into a context. + + The content is copied and digested; it is used for all + future frames until another dictionary is loaded or the + parameters are reset. Loading a null or empty dictionary + returns to no-dictionary mode. + + @param ctx The context. + @param dict The dictionary content. + @param dict_size The dictionary size. + @return Zero, or an error code. + */ + virtual + std::size_t + load_dictionary( + dctx* ctx, + void const* dict, + std::size_t dict_size) const noexcept = 0; + + /** Reference a digested dictionary from a context. + + The dictionary is only referenced and must outlive its + use by the context. Referencing nullptr returns to + no-dictionary mode. + + @param ctx The context. + @param dict The digested dictionary. + @return Zero, or an error code. + */ + virtual + std::size_t + ref_ddict( + dctx* ctx, + ddict const* dict) const noexcept = 0; + + /** Reference a prefix for the next frame. + + The prefix must be the same raw content used with + @ref compress_service::ref_prefix during compression. + It is used once and discarded when the frame ends. + The buffer is only referenced and must remain valid + and unmodified until then. + + @param ctx The context. + @param prefix The prefix content. + @param prefix_size The prefix size. + @return Zero, or an error code. + */ + virtual + std::size_t + ref_prefix( + dctx* ctx, + void const* prefix, + std::size_t prefix_size) const noexcept = 0; + + /** Return the dictionary ID stored within a dictionary. + @param dict The dictionary content. + @param dict_size The dictionary size. + @return The dictionary ID, or zero if the content is + not a conformant dictionary. + */ + virtual + unsigned + get_dict_id_from_dict( + void const* dict, + std::size_t dict_size) const noexcept = 0; + + /** Return the dictionary ID required to decompress a frame. + @param src Start of a frame. + @param src_size Number of bytes available. + @return The dictionary ID, or zero if the frame needs + no dictionary, the ID was omitted, the header + is incomplete, or this is not a frame. + */ + virtual + unsigned + get_dict_id_from_frame( + void const* src, + std::size_t src_size) const noexcept = 0; + + /** Check whether a result is an error code. + @param result A value returned from a function of this service. + @return True if the result encodes an error. + */ + virtual + bool + is_error(std::size_t result) const noexcept = 0; + + /** Convert a result to an error code. + @param result A value returned from a function of this service. + @return The error code, or @ref error::no_error. + */ + virtual + error + get_error_code(std::size_t result) const noexcept = 0; + + /** Return a readable description of a result. + @param result A value returned from a function of this service. + @return Pointer to a description string. + */ + virtual + char const* + get_error_name(std::size_t result) const noexcept = 0; + + /** Return a string description of an error code. + @param c The error code. + @return Pointer to error description string. + */ + virtual + char const* + error_string(error c) const noexcept = 0; + +protected: + void shutdown() override {} +}; + +} // zstd +} // http +} // boost + +#endif diff --git a/include/boost/http/zstd/error.hpp b/include/boost/http/zstd/error.hpp new file mode 100644 index 00000000..5ebc3958 --- /dev/null +++ b/include/boost/http/zstd/error.hpp @@ -0,0 +1,79 @@ +// +// 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 +// + +#ifndef BOOST_HTTP_ZSTD_ERROR_HPP +#define BOOST_HTTP_ZSTD_ERROR_HPP + +#include + +namespace boost { +namespace http { +namespace zstd { + +/** Error codes returned from compression and decompression functions. + + Zstandard functions returning `std::size_t` signal failure + by returning an error code encoded in the result. Use the + service's `is_error` function to test a result and its + `get_error_code` function to convert it to one of these + values. Only `no_error` is a success; every other value + is a failure. +*/ +enum class error +{ + no_error = 0, + generic = 1, + + /* Errors caused by invalid input */ + prefix_unknown = 10, + version_unsupported = 12, + frame_parameter_unsupported = 14, + frame_parameter_window_too_large = 16, + corruption_detected = 20, + checksum_wrong = 22, + literals_header_wrong = 24, + + /* Dictionary errors */ + dictionary_corrupted = 30, + dictionary_wrong = 32, + dictionary_creation_failed = 34, + + /* Parameter errors */ + parameter_unsupported = 40, + parameter_combination_unsupported = 41, + parameter_out_of_bound = 42, + table_log_too_large = 44, + max_symbol_value_too_large = 46, + max_symbol_value_too_small = 48, + cannot_produce_uncompressed_block = 49, + stability_condition_not_respected = 50, + + /* Usage errors */ + stage_wrong = 60, + init_missing = 62, + memory_allocation = 64, + work_space_too_small = 66, + + /* Buffer errors */ + dst_size_too_small = 70, + src_size_wrong = 72, + dst_buffer_null = 74, + + /* Streaming progress errors */ + no_forward_progress_dest_full = 80, + no_forward_progress_input_empty = 82 +}; + +} // zstd +} // http +} // boost + +#include + +#endif diff --git a/include/boost/http/zstd/impl/error.hpp b/include/boost/http/zstd/impl/error.hpp new file mode 100644 index 00000000..e07c8d43 --- /dev/null +++ b/include/boost/http/zstd/impl/error.hpp @@ -0,0 +1,84 @@ +// +// 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 +// + +#ifndef BOOST_HTTP_ZSTD_IMPL_ERROR_HPP +#define BOOST_HTTP_ZSTD_IMPL_ERROR_HPP + +#include + +#include +#include +#include + +namespace boost { + +namespace system { +template<> +struct is_error_code_enum< + ::boost::http::zstd::error> +{ + static bool const value = true; +}; +} // system +} // boost + +namespace std { +template<> +struct is_error_code_enum< + ::boost::http::zstd::error> + : std::true_type {}; +} // std + +namespace boost { +namespace http { +namespace zstd { + +namespace detail { + +struct BOOST_SYMBOL_VISIBLE + error_cat_type + : system::error_category +{ + BOOST_HTTP_DECL const char* name( + ) const noexcept override; + BOOST_HTTP_DECL bool failed( + int) const noexcept override; + BOOST_HTTP_DECL std::string message( + int) const override; + BOOST_HTTP_DECL char const* message( + int, char*, std::size_t + ) const noexcept override; + BOOST_SYSTEM_CONSTEXPR error_cat_type() + : error_category(0x9971e0803a6de4e7) + { + } +}; + +BOOST_HTTP_DECL extern + error_cat_type error_cat; + +} // detail + +inline +BOOST_SYSTEM_CONSTEXPR +system::error_code +make_error_code( + error ev) noexcept +{ + return system::error_code{ + static_cast::type>(ev), + detail::error_cat}; +} + +} // zstd +} // http +} // boost + +#endif diff --git a/include/boost/http/zstd/service.hpp b/include/boost/http/zstd/service.hpp new file mode 100644 index 00000000..bfc8edd6 --- /dev/null +++ b/include/boost/http/zstd/service.hpp @@ -0,0 +1,62 @@ +// +// 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 +// + +#ifndef BOOST_HTTP_ZSTD_SERVICE_HPP +#define BOOST_HTTP_ZSTD_SERVICE_HPP + +#include +#include + +namespace boost { +namespace http { +namespace zstd { + +struct compress_service; +struct decompress_service; + +/** Install the compress service. + + Installs the compress service into the specified execution context. + + @param ctx The execution context to install into. + + @return A reference to the installed compress service. +*/ +BOOST_HTTP_DECL +compress_service& +install_compress_service( + capy::execution_context& ctx); + +/** Install the decompress service. + + Installs the decompress service into the specified execution context. + + @param ctx The execution context to install into. + + @return A reference to the installed decompress service. +*/ +BOOST_HTTP_DECL +decompress_service& +install_decompress_service( + capy::execution_context& ctx); + +/** Install the Zstandard compress and decompress services, if available. + + The services are installed into the system context, + obtained by calling @ref capy::get_system_context. +*/ +BOOST_HTTP_DECL +void +install_zstd_service(); + +} // zstd +} // http +} // boost + +#endif diff --git a/include/boost/http/zstd/types.hpp b/include/boost/http/zstd/types.hpp new file mode 100644 index 00000000..bfd0aadb --- /dev/null +++ b/include/boost/http/zstd/types.hpp @@ -0,0 +1,138 @@ +// +// 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 +// + +#ifndef BOOST_HTTP_ZSTD_TYPES_HPP +#define BOOST_HTTP_ZSTD_TYPES_HPP + +#include + +#include + +namespace boost { +namespace http { +namespace zstd { + +/** Input buffer for streaming operations. + + Describes a region of input data and tracks how much of + it has been consumed. Before a call, set @ref src and + @ref size to the available input and @ref pos to zero. + The service advances @ref pos as input is consumed; if + `pos < size` after a call, the remaining input must be + presented again on the next call. + + @code + boost::http::zstd::in_buffer in{ data.data(), data.size(), 0 }; + @endcode +*/ +struct in_buffer +{ + /** Start of the input buffer. */ + void const* src; + + /** Size of the input buffer in bytes. */ + std::size_t size; + + /** Position where reading stopped. + + Updated by the service; always `0 <= pos <= size`. + */ + std::size_t pos; +}; + +/** Output buffer for streaming operations. + + Describes a region of writable memory and tracks how much + of it has been filled. Before a call, set @ref dst and + @ref size to the available space and @ref pos to zero. + The service advances @ref pos as output is produced. + + @code + boost::http::zstd::out_buffer out{ buf.data(), buf.size(), 0 }; + @endcode +*/ +struct out_buffer +{ + /** Start of the output buffer. */ + void* dst; + + /** Size of the output buffer in bytes. */ + std::size_t size; + + /** Position where writing stopped. + + Updated by the service; always `0 <= pos <= size`. + */ + std::size_t pos; +}; + +/** Bounds of a compression or decompression parameter. + + Returned by @ref compress_service::param_bounds and + @ref decompress_service::param_bounds. The @ref error + field must be tested with the service's `is_error` + function before the bounds are used. +*/ +struct bounds +{ + /** Error status of the query; zero on success. */ + std::size_t error; + + /** Inclusive lower bound of the parameter. */ + int lower_bound; + + /** Inclusive upper bound of the parameter. */ + int upper_bound; +}; + +/** Context reset directives. + + These values select what is reset when a compression + or decompression context is reset. +*/ +enum class reset_directive +{ + /** Abort the frame in progress. + + Parameters and any loaded dictionary are kept and + will be used for the next frame. Never fails. + */ + session_only = 1, + + /** Restore all parameters to their defaults. + + This also drops any dictionary. Fails if a frame + is in progress. + */ + parameters = 2, + + /** Reset the session, then the parameters. */ + session_and_parameters = 3 +}; + +/** Frame content size constants. + + Special values returned by + @ref decompress_service::get_frame_content_size and + accepted by @ref compress_service::set_pledged_src_size. +*/ +enum content_size : unsigned long long +{ + /** The content size is not recorded in the frame header. */ + content_size_unknown = 0ULL - 1, + + /** The frame header is invalid or too short to decode. */ + content_size_error = 0ULL - 2 +}; + +} // zstd +} // http +} // boost + +#endif diff --git a/src/zstd/error.cpp b/src/zstd/error.cpp new file mode 100644 index 00000000..4bcb9bc1 --- /dev/null +++ b/src/zstd/error.cpp @@ -0,0 +1,102 @@ +// +// 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 +// + +#include + +namespace boost { +namespace http { +namespace zstd { +namespace detail { + +const char* +error_cat_type:: +name() const noexcept +{ + return "boost.http.zstd"; +} + +bool +error_cat_type:: +failed(int ev) const noexcept +{ + return ev != 0; +} + +std::string +error_cat_type:: +message(int ev) const +{ + return message(ev, nullptr, 0); +} + +char const* +error_cat_type:: +message( + int ev, + char*, + std::size_t) const noexcept +{ + switch(static_cast(ev)) + { + case error::no_error: return "no_error"; + case error::generic: return "generic"; + case error::prefix_unknown: return "prefix_unknown"; + case error::version_unsupported: return "version_unsupported"; + case error::frame_parameter_unsupported: return "frame_parameter_unsupported"; + case error::frame_parameter_window_too_large: return "frame_parameter_window_too_large"; + case error::corruption_detected: return "corruption_detected"; + case error::checksum_wrong: return "checksum_wrong"; + case error::literals_header_wrong: return "literals_header_wrong"; + case error::dictionary_corrupted: return "dictionary_corrupted"; + case error::dictionary_wrong: return "dictionary_wrong"; + case error::dictionary_creation_failed: return "dictionary_creation_failed"; + case error::parameter_unsupported: return "parameter_unsupported"; + case error::parameter_combination_unsupported: return "parameter_combination_unsupported"; + case error::parameter_out_of_bound: return "parameter_out_of_bound"; + case error::table_log_too_large: return "table_log_too_large"; + case error::max_symbol_value_too_large: return "max_symbol_value_too_large"; + case error::max_symbol_value_too_small: return "max_symbol_value_too_small"; + case error::cannot_produce_uncompressed_block: return "cannot_produce_uncompressed_block"; + case error::stability_condition_not_respected: return "stability_condition_not_respected"; + case error::stage_wrong: return "stage_wrong"; + case error::init_missing: return "init_missing"; + case error::memory_allocation: return "memory_allocation"; + case error::work_space_too_small: return "work_space_too_small"; + case error::dst_size_too_small: return "dst_size_too_small"; + case error::src_size_wrong: return "src_size_wrong"; + case error::dst_buffer_null: return "dst_buffer_null"; + case error::no_forward_progress_dest_full: return "no_forward_progress_dest_full"; + case error::no_forward_progress_input_empty: return "no_forward_progress_input_empty"; + default: + return "unknown"; + } +} + +// msvc 14.0 has a bug that warns about inability +// to use constexpr construction here, even though +// there's no constexpr construction +#if defined(_MSC_VER) && _MSC_VER <= 1900 +# pragma warning( push ) +# pragma warning( disable : 4592 ) +#endif + +#if defined(__cpp_constinit) && __cpp_constinit >= 201907L +constinit error_cat_type error_cat; +#else +error_cat_type error_cat; +#endif + +#if defined(_MSC_VER) && _MSC_VER <= 1900 +# pragma warning( pop ) +#endif + +} // detail +} // zstd +} // http +} // boost diff --git a/src_zstd/compress.cpp b/src_zstd/compress.cpp new file mode 100644 index 00000000..4684811c --- /dev/null +++ b/src_zstd/compress.cpp @@ -0,0 +1,312 @@ +// +// 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 +// + +#include +#include + +#include +#include + +#if ZSTD_VERSION_NUMBER < 10400 +# error "Boost.HTTP requires zstd 1.4.0 or later" +#endif + +namespace boost { +namespace http { +namespace zstd { + +class compress_service_impl + : public compress_service +{ +public: + using key_type = compress_service; + + explicit + compress_service_impl( + capy::execution_context&) noexcept + { + } + + ~compress_service_impl() + { + } + + unsigned + version_number() const noexcept override + { + return ZSTD_versionNumber(); + } + + char const* + version_string() const noexcept override + { + return ZSTD_versionString(); + } + + int + min_level() const noexcept override + { + return ZSTD_minCLevel(); + } + + int + max_level() const noexcept override + { + return ZSTD_maxCLevel(); + } + + int + default_level() const noexcept override + { +#if ZSTD_VERSION_NUMBER >= 10500 + return ZSTD_defaultCLevel(); +#else + return ZSTD_CLEVEL_DEFAULT; +#endif + } + + std::size_t + compress_bound(std::size_t src_size) const noexcept override + { + return ZSTD_compressBound(src_size); + } + + std::size_t + compress( + void* dst, + std::size_t dst_capacity, + void const* src, + std::size_t src_size, + int level) const noexcept override + { + return ZSTD_compress( + dst, + dst_capacity, + src, + src_size, + level); + } + + cctx* + create_cctx() const noexcept override + { + return reinterpret_cast( + ZSTD_createCCtx()); + } + + std::size_t + free_cctx(cctx* ctx) const noexcept override + { + return ZSTD_freeCCtx( + reinterpret_cast(ctx)); + } + + std::size_t + sizeof_cctx(cctx const* ctx) const noexcept override + { + return ZSTD_sizeof_CCtx( + reinterpret_cast(ctx)); + } + + bounds + param_bounds(c_parameter param) const noexcept override + { + auto const b = ZSTD_cParam_getBounds( + static_cast(param)); + return bounds{ b.error, b.lowerBound, b.upperBound }; + } + + std::size_t + set_parameter( + cctx* ctx, + c_parameter param, + int value) const noexcept override + { + return ZSTD_CCtx_setParameter( + reinterpret_cast(ctx), + static_cast(param), + value); + } + + std::size_t + set_pledged_src_size( + cctx* ctx, + unsigned long long pledged_src_size) const noexcept override + { + return ZSTD_CCtx_setPledgedSrcSize( + reinterpret_cast(ctx), + pledged_src_size); + } + + std::size_t + reset( + cctx* ctx, + reset_directive directive) const noexcept override + { + return ZSTD_CCtx_reset( + reinterpret_cast(ctx), + static_cast(directive)); + } + + std::size_t + compress2( + cctx* ctx, + void* dst, + std::size_t dst_capacity, + void const* src, + std::size_t src_size) const noexcept override + { + return ZSTD_compress2( + reinterpret_cast(ctx), + dst, + dst_capacity, + src, + src_size); + } + + std::size_t + compress_stream( + cctx* ctx, + out_buffer& output, + in_buffer& input, + end_directive end_op) const noexcept override + { + ZSTD_outBuffer out{ output.dst, output.size, output.pos }; + ZSTD_inBuffer in{ input.src, input.size, input.pos }; + auto const rs = ZSTD_compressStream2( + reinterpret_cast(ctx), + &out, + &in, + static_cast(end_op)); + output.pos = out.pos; + input.pos = in.pos; + return rs; + } + + std::size_t + stream_in_size() const noexcept override + { + return ZSTD_CStreamInSize(); + } + + std::size_t + stream_out_size() const noexcept override + { + return ZSTD_CStreamOutSize(); + } + + cdict* + create_cdict( + void const* dict, + std::size_t dict_size, + int level) const noexcept override + { + return reinterpret_cast( + ZSTD_createCDict(dict, dict_size, level)); + } + + std::size_t + free_cdict(cdict* dict) const noexcept override + { + return ZSTD_freeCDict( + reinterpret_cast(dict)); + } + + std::size_t + sizeof_cdict(cdict const* dict) const noexcept override + { + return ZSTD_sizeof_CDict( + reinterpret_cast(dict)); + } + + std::size_t + load_dictionary( + cctx* ctx, + void const* dict, + std::size_t dict_size) const noexcept override + { + return ZSTD_CCtx_loadDictionary( + reinterpret_cast(ctx), + dict, + dict_size); + } + + std::size_t + ref_cdict( + cctx* ctx, + cdict const* dict) const noexcept override + { + return ZSTD_CCtx_refCDict( + reinterpret_cast(ctx), + reinterpret_cast(dict)); + } + + std::size_t + ref_prefix( + cctx* ctx, + void const* prefix, + std::size_t prefix_size) const noexcept override + { + return ZSTD_CCtx_refPrefix( + reinterpret_cast(ctx), + prefix, + prefix_size); + } + + unsigned + get_dict_id_from_dict( + void const* dict, + std::size_t dict_size) const noexcept override + { + return ZSTD_getDictID_fromDict(dict, dict_size); + } + + bool + is_error(std::size_t result) const noexcept override + { + return ZSTD_isError(result) != 0; + } + + error + get_error_code(std::size_t result) const noexcept override + { + return static_cast( + ZSTD_getErrorCode(result)); + } + + char const* + get_error_name(std::size_t result) const noexcept override + { + return ZSTD_getErrorName(result); + } + + char const* + error_string(error c) const noexcept override + { + return ZSTD_getErrorString( + static_cast(c)); + } +}; + +compress_service& +install_compress_service(capy::execution_context& ctx) +{ + return ctx.make_service(); +} + +void +install_zstd_service() +{ + install_compress_service(capy::get_system_context()); + install_decompress_service(capy::get_system_context()); +} + +} // zstd +} // http +} // boost diff --git a/src_zstd/decompress.cpp b/src_zstd/decompress.cpp new file mode 100644 index 00000000..209d2a3b --- /dev/null +++ b/src_zstd/decompress.cpp @@ -0,0 +1,286 @@ +// +// 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 +// + +#include +#include + +#include +#include + +#if ZSTD_VERSION_NUMBER < 10400 +# error "Boost.HTTP requires zstd 1.4.0 or later" +#endif + +namespace boost { +namespace http { +namespace zstd { + +class decompress_service_impl + : public decompress_service +{ +public: + using key_type = decompress_service; + + explicit + decompress_service_impl( + capy::execution_context&) noexcept + { + } + + ~decompress_service_impl() + { + } + + unsigned + version_number() const noexcept override + { + return ZSTD_versionNumber(); + } + + char const* + version_string() const noexcept override + { + return ZSTD_versionString(); + } + + std::size_t + decompress( + void* dst, + std::size_t dst_capacity, + void const* src, + std::size_t compressed_size) const noexcept override + { + return ZSTD_decompress( + dst, + dst_capacity, + src, + compressed_size); + } + + unsigned long long + get_frame_content_size( + void const* src, + std::size_t src_size) const noexcept override + { + return ZSTD_getFrameContentSize(src, src_size); + } + + std::size_t + find_frame_compressed_size( + void const* src, + std::size_t src_size) const noexcept override + { + return ZSTD_findFrameCompressedSize(src, src_size); + } + + dctx* + create_dctx() const noexcept override + { + return reinterpret_cast( + ZSTD_createDCtx()); + } + + std::size_t + free_dctx(dctx* ctx) const noexcept override + { + return ZSTD_freeDCtx( + reinterpret_cast(ctx)); + } + + std::size_t + sizeof_dctx(dctx const* ctx) const noexcept override + { + return ZSTD_sizeof_DCtx( + reinterpret_cast(ctx)); + } + + bounds + param_bounds(d_parameter param) const noexcept override + { + auto const b = ZSTD_dParam_getBounds( + static_cast(param)); + return bounds{ b.error, b.lowerBound, b.upperBound }; + } + + std::size_t + set_parameter( + dctx* ctx, + d_parameter param, + int value) const noexcept override + { + return ZSTD_DCtx_setParameter( + reinterpret_cast(ctx), + static_cast(param), + value); + } + + std::size_t + reset( + dctx* ctx, + reset_directive directive) const noexcept override + { + return ZSTD_DCtx_reset( + reinterpret_cast(ctx), + static_cast(directive)); + } + + std::size_t + decompress_dctx( + dctx* ctx, + void* dst, + std::size_t dst_capacity, + void const* src, + std::size_t src_size) const noexcept override + { + return ZSTD_decompressDCtx( + reinterpret_cast(ctx), + dst, + dst_capacity, + src, + src_size); + } + + std::size_t + decompress_stream( + dctx* ctx, + out_buffer& output, + in_buffer& input) const noexcept override + { + ZSTD_outBuffer out{ output.dst, output.size, output.pos }; + ZSTD_inBuffer in{ input.src, input.size, input.pos }; + auto const rs = ZSTD_decompressStream( + reinterpret_cast(ctx), + &out, + &in); + output.pos = out.pos; + input.pos = in.pos; + return rs; + } + + std::size_t + stream_in_size() const noexcept override + { + return ZSTD_DStreamInSize(); + } + + std::size_t + stream_out_size() const noexcept override + { + return ZSTD_DStreamOutSize(); + } + + ddict* + create_ddict( + void const* dict, + std::size_t dict_size) const noexcept override + { + return reinterpret_cast( + ZSTD_createDDict(dict, dict_size)); + } + + std::size_t + free_ddict(ddict* dict) const noexcept override + { + return ZSTD_freeDDict( + reinterpret_cast(dict)); + } + + std::size_t + sizeof_ddict(ddict const* dict) const noexcept override + { + return ZSTD_sizeof_DDict( + reinterpret_cast(dict)); + } + + std::size_t + load_dictionary( + dctx* ctx, + void const* dict, + std::size_t dict_size) const noexcept override + { + return ZSTD_DCtx_loadDictionary( + reinterpret_cast(ctx), + dict, + dict_size); + } + + std::size_t + ref_ddict( + dctx* ctx, + ddict const* dict) const noexcept override + { + return ZSTD_DCtx_refDDict( + reinterpret_cast(ctx), + reinterpret_cast(dict)); + } + + std::size_t + ref_prefix( + dctx* ctx, + void const* prefix, + std::size_t prefix_size) const noexcept override + { + return ZSTD_DCtx_refPrefix( + reinterpret_cast(ctx), + prefix, + prefix_size); + } + + unsigned + get_dict_id_from_dict( + void const* dict, + std::size_t dict_size) const noexcept override + { + return ZSTD_getDictID_fromDict(dict, dict_size); + } + + unsigned + get_dict_id_from_frame( + void const* src, + std::size_t src_size) const noexcept override + { + return ZSTD_getDictID_fromFrame(src, src_size); + } + + bool + is_error(std::size_t result) const noexcept override + { + return ZSTD_isError(result) != 0; + } + + error + get_error_code(std::size_t result) const noexcept override + { + return static_cast( + ZSTD_getErrorCode(result)); + } + + char const* + get_error_name(std::size_t result) const noexcept override + { + return ZSTD_getErrorName(result); + } + + char const* + error_string(error c) const noexcept override + { + return ZSTD_getErrorString( + static_cast(c)); + } +}; + +decompress_service& +install_decompress_service(capy::execution_context& ctx) +{ + return ctx.make_service(); +} + +} // zstd +} // http +} // boost diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt index 9a180a19..b6b8ac47 100644 --- a/test/unit/CMakeLists.txt +++ b/test/unit/CMakeLists.txt @@ -34,6 +34,10 @@ if (TARGET Boost::http_brotli) target_link_libraries(boost_http_tests PRIVATE Boost::http_brotli) endif () +if (TARGET Boost::http_zstd) + target_link_libraries(boost_http_tests PRIVATE Boost::http_zstd) +endif () + # Register individual tests with CTest boost_capy_test_suite_discover_tests(boost_http_tests) diff --git a/test/unit/Jamfile b/test/unit/Jamfile index abeba948..7db4e633 100644 --- a/test/unit/Jamfile +++ b/test/unit/Jamfile @@ -36,6 +36,7 @@ project /boost/url//boost_url [ ac.check-library /boost/http//boost_http_zlib : /boost/http//boost_http_zlib : ] [ ac.check-library /boost/http//boost_http_brotli : /boost/http//boost_http_brotli : ] + [ ac.check-library /boost/http//boost_http_zstd : /boost/http//boost_http_zstd : ] test_helpers_lib . ../.. diff --git a/test/unit/zstd.cpp b/test/unit/zstd.cpp new file mode 100644 index 00000000..a5473523 --- /dev/null +++ b/test/unit/zstd.cpp @@ -0,0 +1,488 @@ +// +// 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 +// + +#include +#include + +#include "test_helpers.hpp" + +#include +#include + +namespace boost { +namespace http { + +class test_context : public capy::execution_context +{ +public: + ~test_context() + { + shutdown(); + destroy(); + } +}; + +struct zstd_test +{ + void + test_error_code() + { + system::error_code ec = zstd::error::no_error; + BOOST_TEST(! ec); + BOOST_TEST(! ec.failed()); + BOOST_TEST_EQ(std::string(ec.category().name()), std::string("boost.http.zstd")); + + ec = zstd::error::corruption_detected; + BOOST_TEST(ec.failed()); + BOOST_TEST(ec == zstd::error::corruption_detected); + BOOST_TEST_EQ(ec.message(), "corruption_detected"); + + ec = static_cast(9999); + BOOST_TEST(ec.failed()); + BOOST_TEST_EQ(ec.message(), "unknown"); + + std::error_code sec = zstd::error::memory_allocation; + BOOST_TEST(sec); + } + +#ifdef BOOST_HTTP_HAS_ZSTD + static + std::string + sample_text() + { + std::string s; + for(int i = 0; i < 100; ++i) + s += "Hello, World! This is a test of zstd compression. "; + return s; + } + + void + test_install() + { + test_context ctx; + auto& csvc = zstd::install_compress_service(ctx); + auto& dsvc = zstd::install_decompress_service(ctx); + + BOOST_TEST(ctx.find_service() == &csvc); + BOOST_TEST(ctx.find_service() == &dsvc); + + BOOST_TEST_GE(csvc.version_number(), 10400u); + BOOST_TEST_EQ(csvc.version_number(), dsvc.version_number()); + BOOST_TEST_EQ(std::string(csvc.version_string()), std::string(dsvc.version_string())); + + BOOST_TEST_LT(csvc.min_level(), 0); + BOOST_TEST_GE(csvc.max_level(), 19); + BOOST_TEST_GE(csvc.default_level(), csvc.min_level()); + BOOST_TEST_LE(csvc.default_level(), csvc.max_level()); + + BOOST_TEST_GT(csvc.stream_in_size(), 0u); + BOOST_TEST_GT(csvc.stream_out_size(), 0u); + BOOST_TEST_GT(dsvc.stream_in_size(), 0u); + BOOST_TEST_GT(dsvc.stream_out_size(), 0u); + } + + void + test_one_shot() + { + test_context ctx; + auto& csvc = zstd::install_compress_service(ctx); + auto& dsvc = zstd::install_decompress_service(ctx); + + auto const input = sample_text(); + + auto const bound = csvc.compress_bound(input.size()); + BOOST_TEST(! csvc.is_error(bound)); + BOOST_TEST_GE(bound, input.size()); + + std::string compressed(bound, '\0'); + auto const n = csvc.compress( + &compressed[0], compressed.size(), + input.data(), input.size(), + csvc.default_level()); + if(! BOOST_TEST(! csvc.is_error(n))) + return; + compressed.resize(n); + BOOST_TEST_GT(compressed.size(), 0u); + BOOST_TEST_LT(compressed.size(), input.size()); + + BOOST_TEST_EQ( + dsvc.get_frame_content_size(compressed.data(), compressed.size()), + input.size()); + BOOST_TEST_EQ( + dsvc.find_frame_compressed_size(compressed.data(), compressed.size()), + compressed.size()); + BOOST_TEST_EQ( + dsvc.get_dict_id_from_frame(compressed.data(), compressed.size()), + 0u); + + std::string output(input.size(), '\0'); + auto const m = dsvc.decompress( + &output[0], output.size(), + compressed.data(), compressed.size()); + if(! BOOST_TEST(! dsvc.is_error(m))) + return; + BOOST_TEST_EQ(m, input.size()); + BOOST_TEST(output == input); + + // a destination which is too small is an error + std::string small(input.size() / 2, '\0'); + auto const r = dsvc.decompress( + &small[0], small.size(), + compressed.data(), compressed.size()); + BOOST_TEST(dsvc.is_error(r)); + BOOST_TEST(dsvc.get_error_code(r) == zstd::error::dst_size_too_small); + } + + void + test_stream() + { + test_context ctx; + auto& csvc = zstd::install_compress_service(ctx); + auto& dsvc = zstd::install_decompress_service(ctx); + + auto const input = sample_text(); + + auto* cctx = csvc.create_cctx(); + if(! BOOST_TEST(cctx != nullptr)) + return; + BOOST_TEST_GT(csvc.sizeof_cctx(cctx), 0u); + BOOST_TEST(! csvc.is_error(csvc.set_parameter( + cctx, zstd::c_parameter::compression_level, 5))); + BOOST_TEST(! csvc.is_error(csvc.set_parameter( + cctx, zstd::c_parameter::checksum_flag, 1))); + BOOST_TEST(! csvc.is_error(csvc.set_pledged_src_size( + cctx, input.size()))); + + // Drive the compressor with a tiny output buffer + // so that every directive needs multiple calls. + std::string compressed; + auto const drive = [&]( + zstd::in_buffer& in, + zstd::end_directive op) + { + char buf[64]; + for(int i = 0; i < 10000; ++i) + { + zstd::out_buffer out{ buf, sizeof(buf), 0 }; + auto const rs = csvc.compress_stream(cctx, out, in, op); + if(! BOOST_TEST(! csvc.is_error(rs))) + return false; + compressed.append(buf, out.pos); + if(op == zstd::end_directive::continue_) + { + if(in.pos == in.size) + return true; + } + else if(rs == 0) + { + return true; + } + } + BOOST_TEST_FAIL(); + return false; + }; + + auto const half = input.size() / 2; + zstd::in_buffer in1{ input.data(), half, 0 }; + if(! drive(in1, zstd::end_directive::continue_)) + return; + zstd::in_buffer empty{ nullptr, 0, 0 }; + if(! drive(empty, zstd::end_directive::flush)) + return; + // flushing emits a decodable block + BOOST_TEST_GT(compressed.size(), 0u); + zstd::in_buffer in2{ input.data() + half, input.size() - half, 0 }; + if(! drive(in2, zstd::end_directive::end)) + return; + BOOST_TEST_LT(compressed.size(), input.size()); + BOOST_TEST_EQ( + dsvc.get_frame_content_size(compressed.data(), compressed.size()), + input.size()); + + // the context can be reused for another frame + BOOST_TEST(! csvc.is_error(csvc.reset( + cctx, zstd::reset_directive::session_only))); + { + std::string second(csvc.compress_bound(input.size()), '\0'); + auto const n = csvc.compress2( + cctx, &second[0], second.size(), + input.data(), input.size()); + BOOST_TEST(! csvc.is_error(n)); + BOOST_TEST_GT(n, 0u); + } + BOOST_TEST(! csvc.is_error(csvc.free_cctx(cctx))); + + // decompress with a tiny output buffer + auto* dctx = dsvc.create_dctx(); + if(! BOOST_TEST(dctx != nullptr)) + return; + BOOST_TEST_GT(dsvc.sizeof_dctx(dctx), 0u); + BOOST_TEST(! dsvc.is_error(dsvc.set_parameter( + dctx, zstd::d_parameter::window_log_max, 27))); + std::string output; + { + zstd::in_buffer in{ compressed.data(), compressed.size(), 0 }; + char buf[64]; + for(int i = 0;; ++i) + { + if(! BOOST_TEST_LT(i, 10000)) + return; + zstd::out_buffer out{ buf, sizeof(buf), 0 }; + auto const rs = dsvc.decompress_stream(dctx, out, in); + if(! BOOST_TEST(! dsvc.is_error(rs))) + return; + output.append(buf, out.pos); + if(rs == 0) + break; + } + BOOST_TEST_EQ(in.pos, in.size); + } + BOOST_TEST(! dsvc.is_error(dsvc.free_dctx(dctx))); + BOOST_TEST(output == input); + } + + void + test_dictionary() + { + test_context ctx; + auto& csvc = zstd::install_compress_service(ctx); + auto& dsvc = zstd::install_decompress_service(ctx); + + // A raw content dictionary; the input starts with it + std::string const dict = + "Hello, World! This is a test of zstd compression. "; + auto const input = sample_text(); + + // raw content is not a conformant dictionary + BOOST_TEST_EQ( + csvc.get_dict_id_from_dict(dict.data(), dict.size()), 0u); + BOOST_TEST_EQ( + dsvc.get_dict_id_from_dict(dict.data(), dict.size()), 0u); + + // compress with a digested dictionary + auto* cd = csvc.create_cdict(dict.data(), dict.size(), 3); + if(! BOOST_TEST(cd != nullptr)) + return; + BOOST_TEST_GT(csvc.sizeof_cdict(cd), 0u); + + auto* cctx = csvc.create_cctx(); + if(! BOOST_TEST(cctx != nullptr)) + return; + BOOST_TEST(! csvc.is_error(csvc.ref_cdict(cctx, cd))); + std::string compressed(csvc.compress_bound(input.size()), '\0'); + auto n = csvc.compress2( + cctx, &compressed[0], compressed.size(), + input.data(), input.size()); + if(! BOOST_TEST(! csvc.is_error(n))) + return; + compressed.resize(n); + BOOST_TEST(! csvc.is_error(csvc.ref_cdict(cctx, nullptr))); + BOOST_TEST(! csvc.is_error(csvc.free_cdict(cd))); + + std::string output(input.size(), '\0'); + auto* dctx = dsvc.create_dctx(); + if(! BOOST_TEST(dctx != nullptr)) + return; + + // without the dictionary, decoding fails + auto m = dsvc.decompress_dctx( + dctx, &output[0], output.size(), + compressed.data(), compressed.size()); + BOOST_TEST(dsvc.is_error(m)); + BOOST_TEST(! dsvc.is_error(dsvc.reset( + dctx, zstd::reset_directive::session_and_parameters))); + + // with a digested dictionary + auto* dd = dsvc.create_ddict(dict.data(), dict.size()); + if(! BOOST_TEST(dd != nullptr)) + return; + BOOST_TEST_GT(dsvc.sizeof_ddict(dd), 0u); + BOOST_TEST(! dsvc.is_error(dsvc.ref_ddict(dctx, dd))); + m = dsvc.decompress_dctx( + dctx, &output[0], output.size(), + compressed.data(), compressed.size()); + BOOST_TEST(! dsvc.is_error(m)); + BOOST_TEST_EQ(m, input.size()); + BOOST_TEST(output == input); + BOOST_TEST(! dsvc.is_error(dsvc.ref_ddict(dctx, nullptr))); + BOOST_TEST(! dsvc.is_error(dsvc.free_ddict(dd))); + + // with a loaded dictionary + std::fill(output.begin(), output.end(), '\0'); + BOOST_TEST(! dsvc.is_error(dsvc.load_dictionary( + dctx, dict.data(), dict.size()))); + m = dsvc.decompress_dctx( + dctx, &output[0], output.size(), + compressed.data(), compressed.size()); + BOOST_TEST(! dsvc.is_error(m)); + BOOST_TEST_EQ(m, input.size()); + BOOST_TEST(output == input); + BOOST_TEST(! dsvc.is_error(dsvc.load_dictionary(dctx, nullptr, 0))); + + // compress with a loaded dictionary + BOOST_TEST(! csvc.is_error(csvc.load_dictionary( + cctx, dict.data(), dict.size()))); + compressed.assign(csvc.compress_bound(input.size()), '\0'); + n = csvc.compress2( + cctx, &compressed[0], compressed.size(), + input.data(), input.size()); + if(! BOOST_TEST(! csvc.is_error(n))) + return; + compressed.resize(n); + BOOST_TEST(! csvc.is_error(csvc.load_dictionary(cctx, nullptr, 0))); + + std::fill(output.begin(), output.end(), '\0'); + BOOST_TEST(! dsvc.is_error(dsvc.load_dictionary( + dctx, dict.data(), dict.size()))); + m = dsvc.decompress_dctx( + dctx, &output[0], output.size(), + compressed.data(), compressed.size()); + BOOST_TEST(! dsvc.is_error(m)); + BOOST_TEST_EQ(m, input.size()); + BOOST_TEST(output == input); + BOOST_TEST(! dsvc.is_error(dsvc.load_dictionary(dctx, nullptr, 0))); + + // with a prefix on both sides + BOOST_TEST(! csvc.is_error(csvc.ref_prefix( + cctx, dict.data(), dict.size()))); + compressed.assign(csvc.compress_bound(input.size()), '\0'); + n = csvc.compress2( + cctx, &compressed[0], compressed.size(), + input.data(), input.size()); + if(! BOOST_TEST(! csvc.is_error(n))) + return; + compressed.resize(n); + BOOST_TEST(! csvc.is_error(csvc.free_cctx(cctx))); + + std::fill(output.begin(), output.end(), '\0'); + BOOST_TEST(! dsvc.is_error(dsvc.ref_prefix( + dctx, dict.data(), dict.size()))); + m = dsvc.decompress_dctx( + dctx, &output[0], output.size(), + compressed.data(), compressed.size()); + BOOST_TEST(! dsvc.is_error(m)); + BOOST_TEST_EQ(m, input.size()); + BOOST_TEST(output == input); + BOOST_TEST(! dsvc.is_error(dsvc.free_dctx(dctx))); + } + + void + test_errors() + { + test_context ctx; + auto& csvc = zstd::install_compress_service(ctx); + auto& dsvc = zstd::install_decompress_service(ctx); + + // invalid frame + std::string const garbage = "this is definitely not a zstd frame"; + std::string output(64, '\0'); + auto const rs = dsvc.decompress( + &output[0], output.size(), + garbage.data(), garbage.size()); + BOOST_TEST(dsvc.is_error(rs)); + BOOST_TEST(dsvc.get_error_code(rs) == zstd::error::prefix_unknown); + BOOST_TEST(dsvc.get_error_name(rs) != nullptr); + BOOST_TEST_EQ( + std::string(dsvc.error_string(zstd::error::prefix_unknown)), + std::string(dsvc.get_error_name(rs))); + BOOST_TEST_EQ( + std::string(csvc.error_string(zstd::error::prefix_unknown)), + std::string(dsvc.error_string(zstd::error::prefix_unknown))); + BOOST_TEST_EQ( + dsvc.get_frame_content_size(garbage.data(), garbage.size()), + zstd::content_size_error); + BOOST_TEST(dsvc.is_error( + dsvc.find_frame_compressed_size(garbage.data(), garbage.size()))); + + // the error code converts to a system::error_code + system::error_code ec = dsvc.get_error_code(rs); + BOOST_TEST(ec.failed()); + BOOST_TEST_EQ(ec.message(), "prefix_unknown"); + + // successful results are not errors + BOOST_TEST(! csvc.is_error(0)); + BOOST_TEST(csvc.get_error_code(0) == zstd::error::no_error); + BOOST_TEST(! csvc.is_error(12345)); + BOOST_TEST(! dsvc.is_error(0)); + BOOST_TEST(dsvc.get_error_code(12345) == zstd::error::no_error); + + // parameter bounds + auto const cb = csvc.param_bounds(zstd::c_parameter::compression_level); + BOOST_TEST(! csvc.is_error(cb.error)); + BOOST_TEST_EQ(cb.lower_bound, csvc.min_level()); + BOOST_TEST_EQ(cb.upper_bound, csvc.max_level()); + + auto const db = dsvc.param_bounds(zstd::d_parameter::window_log_max); + BOOST_TEST(! dsvc.is_error(db.error)); + BOOST_TEST_LT(db.lower_bound, db.upper_bound); + + // out of bound parameter + auto* cctx = csvc.create_cctx(); + if(! BOOST_TEST(cctx != nullptr)) + return; + auto const pr = csvc.set_parameter( + cctx, zstd::c_parameter::window_log, 1); + BOOST_TEST(csvc.is_error(pr)); + BOOST_TEST(csvc.get_error_code(pr) == + zstd::error::parameter_out_of_bound); + BOOST_TEST(! csvc.is_error(csvc.free_cctx(cctx))); + + // frames larger than the window limit are + // rejected in streaming mode + { + auto const input = sample_text(); + std::string compressed(csvc.compress_bound(input.size()), '\0'); + auto const n = csvc.compress( + &compressed[0], compressed.size(), + input.data(), input.size(), + csvc.default_level()); + if(! BOOST_TEST(! csvc.is_error(n))) + return; + compressed.resize(n); + + auto* dctx = dsvc.create_dctx(); + if(! BOOST_TEST(dctx != nullptr)) + return; + BOOST_TEST(! dsvc.is_error(dsvc.set_parameter( + dctx, zstd::d_parameter::window_log_max, 10))); + zstd::in_buffer in{ compressed.data(), compressed.size(), 0 }; + zstd::out_buffer out{ &output[0], output.size(), 0 }; + auto const sr = dsvc.decompress_stream(dctx, out, in); + BOOST_TEST(dsvc.is_error(sr)); + BOOST_TEST(dsvc.get_error_code(sr) == + zstd::error::frame_parameter_window_too_large); + BOOST_TEST(! dsvc.is_error(dsvc.free_dctx(dctx))); + } + + // null pointers are accepted by the free functions + BOOST_TEST(! csvc.is_error(csvc.free_cctx(nullptr))); + BOOST_TEST(! csvc.is_error(csvc.free_cdict(nullptr))); + BOOST_TEST(! dsvc.is_error(dsvc.free_dctx(nullptr))); + BOOST_TEST(! dsvc.is_error(dsvc.free_ddict(nullptr))); + } +#endif + + void + run() + { + test_error_code(); + #ifdef BOOST_HTTP_HAS_ZSTD + test_install(); + test_one_shot(); + test_stream(); + test_dictionary(); + test_errors(); + #endif + } +}; + +TEST_SUITE(zstd_test, "boost.http.zstd"); + +} // namespace http +} // namespace boost diff --git a/test/unit/zstd/compress.cpp b/test/unit/zstd/compress.cpp new file mode 100644 index 00000000..27c43509 --- /dev/null +++ b/test/unit/zstd/compress.cpp @@ -0,0 +1,11 @@ +// +// 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 +// + +// Test that header file is self-contained. +#include diff --git a/test/unit/zstd/decompress.cpp b/test/unit/zstd/decompress.cpp new file mode 100644 index 00000000..adbdca49 --- /dev/null +++ b/test/unit/zstd/decompress.cpp @@ -0,0 +1,11 @@ +// +// 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 +// + +// Test that header file is self-contained. +#include diff --git a/test/unit/zstd/zstd_error.cpp b/test/unit/zstd/zstd_error.cpp new file mode 100644 index 00000000..519525a4 --- /dev/null +++ b/test/unit/zstd/zstd_error.cpp @@ -0,0 +1,11 @@ +// +// 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 +// + +// Test that header file is self-contained. +#include diff --git a/test/unit/zstd/zstd_types.cpp b/test/unit/zstd/zstd_types.cpp new file mode 100644 index 00000000..73562fd4 --- /dev/null +++ b/test/unit/zstd/zstd_types.cpp @@ -0,0 +1,11 @@ +// +// 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 +// + +// Test that header file is self-contained. +#include