--- /dev/null
+// Copyright (C) 2022 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include "zstd.hpp"
+
+#include <zstd.h>
+
+namespace util {
+
+nonstd::expected<void, std::string>
+zstd_compress(nonstd::span<const uint8_t> input,
+ util::Bytes& output,
+ int8_t compression_level)
+{
+ const size_t original_output_size = output.size();
+ const size_t compress_bound = zstd_compress_bound(input.size());
+ output.resize(original_output_size + compress_bound);
+
+ const size_t ret = ZSTD_compress(&output[original_output_size],
+ compress_bound,
+ input.data(),
+ input.size(),
+ compression_level);
+ if (ZSTD_isError(ret)) {
+ return nonstd::make_unexpected(ZSTD_getErrorName(ret));
+ }
+
+ output.resize(original_output_size + ret);
+ return {};
+}
+
+nonstd::expected<void, std::string>
+zstd_decompress(nonstd::span<const uint8_t> input,
+ util::Bytes& output,
+ size_t original_size)
+{
+ const size_t original_output_size = output.size();
+
+ output.resize(original_output_size + original_size);
+ const size_t ret = ZSTD_decompress(
+ &output[original_output_size], original_size, input.data(), input.size());
+ if (ZSTD_isError(ret)) {
+ return nonstd::make_unexpected(ZSTD_getErrorName(ret));
+ }
+
+ output.resize(original_output_size + ret);
+
+ return {};
+}
+
+size_t
+zstd_compress_bound(size_t input_size)
+{
+ return ZSTD_compressBound(input_size);
+}
+
+std::tuple<int8_t, std::string>
+zstd_supported_compression_level(int8_t wanted_level)
+{
+ // libzstd 1.3.4 and newer support negative levels. However, the query
+ // function ZSTD_minCLevel did not appear until 1.3.6, so perform detection
+ // based on version instead.
+ if (ZSTD_versionNumber() < 10304 && wanted_level < 1) {
+ return {1, "minimum level supported by libzstd"};
+ }
+
+ const int8_t level = std::min<int>(wanted_level, ZSTD_maxCLevel());
+ if (level != wanted_level) {
+ return {level, "max libzstd level"};
+ }
+
+ return {level, {}};
+}
+
+} // namespace util
--- /dev/null
+// Copyright (C) 2022 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#pragma once
+
+#include <util/Bytes.hpp>
+
+#include <third_party/nonstd/expected.hpp>
+#include <third_party/nonstd/span.hpp>
+
+#include <cstdint>
+#include <string>
+#include <tuple>
+
+namespace util {
+
+[[nodiscard]] nonstd::expected<void, std::string>
+zstd_compress(nonstd::span<const uint8_t> input,
+ util::Bytes& output,
+ int8_t compression_level);
+
+[[nodiscard]] nonstd::expected<void, std::string> zstd_decompress(
+ nonstd::span<const uint8_t> input, util::Bytes& output, size_t original_size);
+
+size_t zstd_compress_bound(size_t input_size);
+
+std::tuple<int8_t, std::string>
+zstd_supported_compression_level(int8_t wanted_level);
+
+} // namespace util
--- /dev/null
+// Copyright (C) 2019-2022 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include <TestUtil.hpp>
+#include <util/Bytes.hpp>
+#include <util/zstd.hpp>
+
+#include <third_party/doctest.h>
+
+#include <string>
+
+using TestUtil::TestContext;
+
+const util::Bytes compressed_ab{
+ 0x28, 0xb5, 0x2f, 0xfd, 0x20, 0x02, 0x11, 0x00, 0x00, 0x61, 0x62};
+
+TEST_CASE("util::zstd_compress")
+{
+ TestContext test_context;
+
+ util::Bytes output{'x'};
+ auto result = util::zstd_compress(util::Bytes{'a', 'b'}, output, 1);
+ CHECK(result);
+ CHECK(output.size() == 12);
+ util::Bytes expected{'x'};
+ expected.insert(expected.end(), compressed_ab.begin(), compressed_ab.end());
+ CHECK(output == expected);
+}
+
+TEST_CASE("util::zstd_decompress")
+{
+ TestContext test_context;
+
+ util::Bytes input = compressed_ab;
+ util::Bytes output{'x'};
+ auto result = util::zstd_decompress(input, output, 2);
+ CHECK(result);
+ CHECK(output == util::Bytes{'x', 'a', 'b'});
+}
+
+TEST_CASE("ZSTD roundtrip")
+{
+ TestContext test_context;
+
+ const util::Bytes data{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+ const size_t copies = 10000;
+ util::Bytes original_input;
+ for (size_t i = 0; i < copies; i++) {
+ original_input.insert(original_input.end(), data.begin(), data.end());
+ }
+
+ util::Bytes output;
+ auto result = util::zstd_compress(original_input, output, 1);
+ CHECK(result);
+ CHECK(output.size() < 100);
+
+ util::Bytes decompressed_input;
+ result =
+ util::zstd_decompress(output, decompressed_input, copies * data.size());
+ CHECK(result);
+ CHECK(decompressed_input == original_input);
+}