diff --git a/builtin-functions/kphp-light/stdlib/string-functions.txt b/builtin-functions/kphp-light/stdlib/string-functions.txt index da7249066a..238be83d57 100644 --- a/builtin-functions/kphp-light/stdlib/string-functions.txt +++ b/builtin-functions/kphp-light/stdlib/string-functions.txt @@ -135,20 +135,16 @@ function unpack ($pattern ::: string, $data ::: string) ::: mixed[] | false; */ function base_convert ($number ::: string, $frombase ::: int, $tobase ::: int) ::: string; -// === UNSUPPORTED === - -/** @kphp-extern-func-info stub generation-required */ -function str_getcsv($str ::: string, string $delimiter ::: string = ",", string $enclosure ::: string = "\"", string $escape ::: string = "\\") ::: mixed[] | false; - -/** @kphp-extern-func-info stub generation-required */ function zstd_compress(string $data, int $level = 3) ::: string | false; -/** @kphp-extern-func-info stub generation-required */ function zstd_uncompress(string $data) ::: string | false; -/** @kphp-extern-func-info stub generation-required */ function zstd_compress_dict(string $data, string $dict) ::: string | false; -/** @kphp-extern-func-info stub generation-required */ function zstd_uncompress_dict(string $data, string $dict) ::: string | false; +// === UNSUPPORTED === + +/** @kphp-extern-func-info stub generation-required */ +function str_getcsv($str ::: string, string $delimiter ::: string = ",", string $enclosure ::: string = "\"", string $escape ::: string = "\\") ::: mixed[] | false; + /** @kphp-extern-func-info stub generation-required */ function getimagesize ($name ::: string) ::: mixed; diff --git a/runtime-light/runtime-light.cmake b/runtime-light/runtime-light.cmake index 3cc64e6e03..d3745ef234 100644 --- a/runtime-light/runtime-light.cmake +++ b/runtime-light/runtime-light.cmake @@ -55,9 +55,9 @@ vk_add_library_pic(runtime-light-pic OBJECT ${RUNTIME_LIGHT_SRC}) target_compile_options(runtime-light-pic PUBLIC ${RUNTIME_LIGHT_COMPILE_FLAGS}) target_link_libraries(runtime-light-pic PUBLIC vk::pic::libc-alloc-wrapper) # it's mandatory to have alloc-wrapper first in the list of link libraries since we # want to use its symbols in all other libraries -target_link_libraries(runtime-light-pic PUBLIC KPHP_TIMELIB::pic::timelib PCRE2::pic::pcre2 ZLIB::pic::zlib) # third parties +target_link_libraries(runtime-light-pic PUBLIC KPHP_TIMELIB::pic::timelib PCRE2::pic::pcre2 ZLIB::pic::zlib ZSTD::pic::zstd) # third parties -set(RUNTIME_LIGHT_LINK_LIBS "${KPHP_TIMELIB_PIC_LIBRARIES} ${PCRE2_PIC_LIBRARIES} ${ZLIB_PIC_LIBRARIES}") +set(RUNTIME_LIGHT_LINK_LIBS "${KPHP_TIMELIB_PIC_LIBRARIES} ${PCRE2_PIC_LIBRARIES} ${ZLIB_PIC_LIBRARIES} ${ZSTD_PIC_LIBRARIES}") # ================================================================================================= diff --git a/runtime-light/stdlib/stdlib.cmake b/runtime-light/stdlib/stdlib.cmake index a176dac8f5..4ab1cfd301 100644 --- a/runtime-light/stdlib/stdlib.cmake +++ b/runtime-light/stdlib/stdlib.cmake @@ -38,4 +38,5 @@ prepend( time/time-functions.cpp time/time-state.cpp time/timelib-functions.cpp - zlib/zlib-functions.cpp) + zlib/zlib-functions.cpp + zstd/zstd-functions.cpp) diff --git a/runtime-light/stdlib/zstd/zstd-functions.cpp b/runtime-light/stdlib/zstd/zstd-functions.cpp new file mode 100644 index 0000000000..d8a986e02f --- /dev/null +++ b/runtime-light/stdlib/zstd/zstd-functions.cpp @@ -0,0 +1,139 @@ +// Compiler for PHP (aka KPHP) +// Copyright (c) 2025 LLC «V Kontakte» +// Distributed under the GPL v3 License, see LICENSE.notice.txt + +#include "runtime-light/stdlib/zstd/zstd-functions.h" + +#include +#include +#include +#include + +#define ZSTD_STATIC_LINKING_ONLY +#include "zstd/zstd.h" + +#include "common/containers/final_action.h" +#include "runtime-common/core/allocator/script-malloc-interface.h" +#include "runtime-common/core/runtime-core.h" +#include "runtime-common/stdlib/string/string-context.h" +#include "runtime-light/stdlib/diagnostics/logs.h" + +namespace { + +static_assert(2 * ZSTD_BLOCKSIZE_MAX < StringLibContext::STATIC_BUFFER_LENGTH, "double block size is expected to be less then buffer size"); + +constexpr ZSTD_customMem zstd_allocator{[](void*, size_t size) noexcept { return kphp::memory::script::alloc(size); }, + [](void*, void* ptr) noexcept { return kphp::memory::script::free(ptr); }}; + +} // namespace + +namespace kphp::zstd { + +std::optional compress(std::span data, int64_t level, std::span dict) noexcept { + const int32_t min_level{ZSTD_minCLevel()}; + const int32_t max_level{ZSTD_maxCLevel()}; + if (level < min_level || max_level < level) { + kphp::log::warning("zstd_compress: compression level ({}) must be within [{}..{}]", level, min_level, max_level); + return {}; + } + + ZSTD_CCtx* ctx{ZSTD_createCCtx_advanced(zstd_allocator)}; + if (!ctx) { + kphp::log::warning("zstd_compress: can not create context"); + return {}; + } + const auto finalizer{vk::finally([&ctx]() noexcept { ZSTD_freeCCtx(ctx); })}; + + size_t result{ZSTD_CCtx_setParameter(ctx, ZSTD_c_compressionLevel, static_cast(level))}; + if (ZSTD_isError(result)) { + kphp::log::warning("zstd_compress: can not init context: {}", ZSTD_getErrorName(result)); + return {}; + } + + result = ZSTD_CCtx_loadDictionary_byReference(ctx, dict.data(), dict.size()); + if (ZSTD_isError(result)) { + kphp::log::warning("zstd_compress: can not load dict: {}", ZSTD_getErrorName(result)); + return {}; + } + + kphp::log::assertion(ZSTD_CStreamOutSize() <= StringLibContext::STATIC_BUFFER_LENGTH); + ZSTD_outBuffer out{StringLibContext::get().static_buf.get(), StringLibContext::STATIC_BUFFER_LENGTH, 0}; + ZSTD_inBuffer in{data.data(), data.size(), 0}; + + string encoded_string{}; + do { + result = ZSTD_compressStream2(ctx, std::addressof(out), std::addressof(in), ZSTD_e_end); + if (ZSTD_isError(result)) { + kphp::log::warning("zstd_compress: got zstd stream compression error: {}", ZSTD_getErrorName(result)); + return {}; + } + encoded_string.append(static_cast(out.dst), out.pos); + out.pos = 0; + } while (result); + return encoded_string; +} + +std::optional uncompress(std::span data, std::span dict) noexcept { + auto size{ZSTD_getFrameContentSize(data.data(), data.size())}; + if (size == ZSTD_CONTENTSIZE_ERROR) { + kphp::log::warning("zstd_uncompress: it was not compressed by zstd"); + return {}; + } + + ZSTD_DCtx* ctx{ZSTD_createDCtx_advanced(zstd_allocator)}; + if (!ctx) { + kphp::log::warning("zstd_uncompress: can not create context"); + return {}; + } + const auto finalizer{vk::finally([&ctx]() noexcept { ZSTD_freeDCtx(ctx); })}; + + size_t result{ZSTD_DCtx_loadDictionary_byReference(ctx, dict.data(), dict.size())}; + if (ZSTD_isError(result)) { + kphp::log::warning("zstd_uncompress: can not load dict: {}", ZSTD_getErrorName(result)); + return {}; + } + + if (size != ZSTD_CONTENTSIZE_UNKNOWN) { + if (size > string::max_size()) { + kphp::log::warning("zstd_uncompress: trying to uncompress too large data"); + return {}; + } + string decompressed{static_cast(size), false}; + result = ZSTD_decompressDCtx(ctx, decompressed.buffer(), size, data.data(), data.size()); + if (ZSTD_isError(result)) { + kphp::log::warning("zstd_uncompress: got zstd error: {}", ZSTD_getErrorName(result)); + return {}; + } + return decompressed; + } + + if (ZSTD_isError(result)) { + kphp::log::warning("zstd_uncompress: can not init stream: {}", ZSTD_getErrorName(result)); + return {}; + } + + kphp::log::assertion(ZSTD_DStreamOutSize() <= StringLibContext::STATIC_BUFFER_LENGTH); + ZSTD_inBuffer in{data.data(), data.size(), 0}; + ZSTD_outBuffer out{StringLibContext::get().static_buf.get(), StringLibContext::STATIC_BUFFER_LENGTH, 0}; + + string decoded_string{}; + while (in.pos < in.size) { + if (out.pos == out.size) { + decoded_string.append(static_cast(out.dst), static_cast(out.pos)); + out.pos = 0; + } + + result = ZSTD_decompressStream(ctx, std::addressof(out), std::addressof(in)); + if (ZSTD_isError(result)) { + kphp::log::warning("zstd_uncompress: can not decompress stream: {}", ZSTD_getErrorName(result)); + return {}; + } + if (result == 0) { + break; + } + } + decoded_string.append(static_cast(out.dst), static_cast(out.pos)); + return decoded_string; +} + +} // namespace kphp::zstd diff --git a/runtime-light/stdlib/zstd/zstd-functions.h b/runtime-light/stdlib/zstd/zstd-functions.h new file mode 100644 index 0000000000..78ea54d115 --- /dev/null +++ b/runtime-light/stdlib/zstd/zstd-functions.h @@ -0,0 +1,57 @@ +// Compiler for PHP (aka KPHP) +// Copyright (c) 2025 LLC «V Kontakte» +// Distributed under the GPL v3 License, see LICENSE.notice.txt + +#pragma once + +#include +#include +#include +#include + +#include "runtime-common/core/runtime-core.h" + +namespace kphp::zstd { + +inline constexpr int64_t DEFAULT_COMPRESS_LEVEL = 3; + +std::optional compress(std::span data, int64_t level = DEFAULT_COMPRESS_LEVEL, + std::span dict = std::span{}) noexcept; + +std::optional uncompress(std::span data, std::span dict = std::span{}) noexcept; + +} // namespace kphp::zstd + +inline Optional f$zstd_compress(const string& data, int64_t level = kphp::zstd::DEFAULT_COMPRESS_LEVEL) noexcept { + auto res{kphp::zstd::compress({reinterpret_cast(data.c_str()), static_cast(data.size())}, level)}; + if (!res) [[unlikely]] { + return false; + } + return res.value(); +} + +inline Optional f$zstd_uncompress(const string& data) noexcept { + auto res{kphp::zstd::uncompress({reinterpret_cast(data.c_str()), static_cast(data.size())})}; + if (!res) [[unlikely]] { + return false; + } + return res.value(); +} + +inline Optional f$zstd_compress_dict(const string& data, const string& dict) noexcept { + auto res{kphp::zstd::compress({reinterpret_cast(data.c_str()), static_cast(data.size())}, kphp::zstd::DEFAULT_COMPRESS_LEVEL, + {reinterpret_cast(dict.c_str()), static_cast(dict.size())})}; + if (!res) [[unlikely]] { + return false; + } + return res.value(); +} + +inline Optional f$zstd_uncompress_dict(const string& data, const string& dict) noexcept { + auto res{kphp::zstd::uncompress({reinterpret_cast(data.c_str()), static_cast(data.size())}, + {reinterpret_cast(dict.c_str()), static_cast(dict.size())})}; + if (!res) [[unlikely]] { + return false; + } + return res.value(); +} diff --git a/tests/phpt/zstd/1_compress.php b/tests/phpt/zstd/1_compress.php index 2efb816a03..f4d4fbf3f3 100644 --- a/tests/phpt/zstd/1_compress.php +++ b/tests/phpt/zstd/1_compress.php @@ -1,4 +1,4 @@ -@ok k2_skip +@ok