diff --git a/builtin-functions/kphp-light/stdlib/zlib-functions.txt b/builtin-functions/kphp-light/stdlib/zlib-functions.txt index c340c48be7..2121dc2f37 100644 --- a/builtin-functions/kphp-light/stdlib/zlib-functions.txt +++ b/builtin-functions/kphp-light/stdlib/zlib-functions.txt @@ -6,23 +6,6 @@ define('ZLIB_ENCODING_RAW', -0x0f); define('ZLIB_ENCODING_DEFLATE', 0x0f); define('ZLIB_ENCODING_GZIP', 0x1f); -function gzcompress ($str ::: string, $level ::: int = -1): string; - -function gzuncompress ($str ::: string): string; - -function gzencode ($str ::: string, $level ::: int = -1) ::: string; - -function gzdecode ($str ::: string) ::: string; - -function gzdeflate ($str ::: string, $level ::: int = -1) ::: string; - -function gzinflate ($str ::: string) ::: string; - -// ===== UNSUPPORTED ===== - -/** @kphp-extern-func-info stub generation-required */ -function md5_file ($s ::: string, $raw_output ::: bool = false) ::: string | false; - define('ZLIB_NO_FLUSH', 0); define('ZLIB_PARTIAL_FLUSH', 1); define('ZLIB_SYNC_FLUSH', 2); @@ -37,15 +20,28 @@ define('ZLIB_RLE', 3); define('ZLIB_FIXED', 4); define('ZLIB_DEFAULT_STRATEGY', 0); -/** @kphp-generate-stub-class */ +function gzcompress ($str ::: string, $level ::: int = -1): string; + +function gzuncompress ($str ::: string): string; + +function gzencode ($str ::: string, $level ::: int = -1) ::: string; + +function gzdecode ($str ::: string) ::: string; + +function gzdeflate ($str ::: string, $level ::: int = -1) ::: string; + +function gzinflate ($str ::: string) ::: string; + +function deflate_init(int $encoding, array $options = []) ::: ?DeflateContext; + +function deflate_add(DeflateContext $context, string $data, int $flush_mode = ZLIB_SYNC_FLUSH) ::: string | false; + final class DeflateContext { - /** @kphp-extern-func-info stub generation-required */ private function __construct() ::: DeflateContext; } -// todo: deflate_init php signature has type array instead mixed -/** @kphp-extern-func-info stub generation-required */ -function deflate_init(int $encoding, mixed $options = []) ::: ?DeflateContext; -/** @kphp-extern-func-info stub generation-required */ -function deflate_add(DeflateContext $context, string $data, int $flush_mode = ZLIB_SYNC_FLUSH) ::: string | false; +// ===== UNSUPPORTED ===== + +/** @kphp-extern-func-info stub generation-required */ +function md5_file ($s ::: string, $raw_output ::: bool = false) ::: string | false; diff --git a/runtime-light/stdlib/zlib/deflate-context.h b/runtime-light/stdlib/zlib/deflate-context.h new file mode 100644 index 0000000000..4b030981d2 --- /dev/null +++ b/runtime-light/stdlib/zlib/deflate-context.h @@ -0,0 +1,26 @@ +// Compiler for PHP (aka KPHP) +// Copyright (c) 2025 LLC «V Kontakte» +// Distributed under the GPL v3 License, see LICENSE.notice.txt + +#pragma once + +#include "zlib/zlib.h" + +#include "runtime-common/core/class-instance/refcountable-php-classes.h" +#include "runtime-common/stdlib/visitors/dummy-visitor-methods.h" + +struct C$DeflateContext : public refcountable_php_classes, private DummyVisitorMethods { + C$DeflateContext() noexcept = default; + using DummyVisitorMethods::accept; + + C$DeflateContext(const C$DeflateContext&) = delete; + C$DeflateContext(C$DeflateContext&&) = delete; + C$DeflateContext& operator=(const C$DeflateContext&) = delete; + C$DeflateContext& operator=(C$DeflateContext&&) = delete; + + ~C$DeflateContext() { + deflateEnd(std::addressof(stream)); + } + + z_stream stream{}; +}; diff --git a/runtime-light/stdlib/zlib/zlib-functions.cpp b/runtime-light/stdlib/zlib/zlib-functions.cpp index d0783e0bee..45ac6d02b4 100644 --- a/runtime-light/stdlib/zlib/zlib-functions.cpp +++ b/runtime-light/stdlib/zlib/zlib-functions.cpp @@ -15,6 +15,7 @@ #include "zlib/zlib.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-light/stdlib/diagnostics/logs.h" #include "runtime-light/stdlib/string/string-state.h" @@ -36,6 +37,18 @@ voidpf zlib_static_alloc(voidpf opaque, uInt items, uInt size) noexcept { void zlib_static_free([[maybe_unused]] voidpf opaque, [[maybe_unused]] voidpf address) noexcept {} +voidpf zlib_dynamic_alloc([[maybe_unused]] voidpf opaque, uInt items, uInt size) noexcept { + auto* mem{kphp::memory::script::calloc(items, size)}; + if (mem == nullptr) [[unlikely]] { + kphp::log::warning("zlib dynamic alloc: can't allocate {} bytes", items * size); + } + return mem; +} + +void zlib_dynamic_free([[maybe_unused]] voidpf opaque, voidpf address) noexcept { + kphp::memory::script::free(address); +} + } // namespace namespace kphp::zlib { @@ -73,8 +86,7 @@ std::optional encode(std::span data, int64_t level, int64_t zstrm.avail_out = out_size_upper_bound; zstrm.next_out = reinterpret_cast(runtime_ctx.static_SB.buffer()); - const auto deflate_res{deflate(std::addressof(zstrm), Z_FINISH)}; - if (deflate_res != Z_STREAM_END) [[unlikely]] { + if (const auto deflate_res{deflate(std::addressof(zstrm), Z_FINISH)}; deflate_res != Z_STREAM_END) [[unlikely]] { kphp::log::warning("can't encode data of length {} due to zlib error {}", data.size(), deflate_res); return {}; } @@ -104,9 +116,8 @@ std::optional decode(std::span data, int64_t encoding) noexc runtime_ctx.static_SB.clean().reserve(StringInstanceState::STATIC_BUFFER_LENGTH); zstrm.avail_out = StringInstanceState::STATIC_BUFFER_LENGTH; zstrm.next_out = reinterpret_cast(runtime_ctx.static_SB.buffer()); - const auto inflate_res{inflate(std::addressof(zstrm), Z_NO_FLUSH)}; - if (inflate_res != Z_STREAM_END) [[unlikely]] { + if (const auto inflate_res{inflate(std::addressof(zstrm), Z_NO_FLUSH)}; inflate_res != Z_STREAM_END) [[unlikely]] { kphp::log::warning("can't decode data of length {} due to zlib error {}", data.size(), inflate_res); return {}; } @@ -115,3 +126,152 @@ std::optional decode(std::span data, int64_t encoding) noexc } } // namespace kphp::zlib + +class_instance f$deflate_init(int64_t encoding, const array& options) noexcept { + int32_t level{-1}; + int32_t memory{8}; + int32_t window{15}; + auto strategy{Z_DEFAULT_STRATEGY}; + constexpr auto extract_int_option{[](int32_t lbound, int32_t ubound, const array_iterator& option, int32_t& dst) noexcept { + if (const mixed & value{option.get_value()}; value.is_int() && value.as_int() >= lbound && value.as_int() <= ubound) { + dst = value.as_int(); + return true; + } else { + kphp::log::warning("option {} should be number between {}..{}", option.get_string_key().c_str(), lbound, ubound); + return false; + } + }}; + + switch (encoding) { + case kphp::zlib::ENCODING_RAW: + case kphp::zlib::ENCODING_DEFLATE: + case kphp::zlib::ENCODING_GZIP: + break; + default: + kphp::log::warning("encoding should be one of ZLIB_ENCODING_RAW, ZLIB_ENCODING_DEFLATE, ZLIB_ENCODING_GZIP"); + return {}; + } + + for (const auto& option : options) { + if (!option.is_string_key()) { + kphp::log::warning("unsupported option"); + return {}; + } + + const auto& key{option.get_string_key()}; + if (std::string_view{key.c_str(), key.size()} == "level") { + if (!extract_int_option(-1, 9, option, level)) { + return {}; + } + } else if (std::string_view{key.c_str(), key.size()} == "memory") { + if (!extract_int_option(1, 9, option, memory)) { + return {}; + } + } else if (std::string_view{key.c_str(), key.size()} == "window") { + if (!extract_int_option(8, 15, option, window)) { + return {}; + } + } else if (std::string_view{key.c_str(), key.size()} == "strategy") { + if (mixed value{option.get_value()}; value.is_int()) { + switch (value.as_int()) { + case Z_FILTERED: + case Z_HUFFMAN_ONLY: + case Z_RLE: + case Z_FIXED: + case Z_DEFAULT_STRATEGY: + strategy = value.as_int(); + break; + default: + kphp::log::warning("option strategy should be one of ZLIB_FILTERED, ZLIB_HUFFMAN_ONLY, ZLIB_RLE, ZLIB_FIXED or ZLIB_DEFAULT_STRATEGY"); + return {}; + } + } else { + kphp::log::warning("option strategy should be one of ZLIB_FILTERED, ZLIB_HUFFMAN_ONLY, ZLIB_RLE, ZLIB_FIXED or ZLIB_DEFAULT_STRATEGY"); + return {}; + } + } else if (std::string_view{key.c_str(), key.size()} == "dictionary") { + kphp::log::warning("option dictionary isn't supported yet"); + return {}; + } else { + kphp::log::warning("unknown option name \"{}\"", option.get_string_key().c_str()); + return {}; + } + } + + class_instance context; + context.alloc(); + + z_stream* stream{std::addressof(context.get()->stream)}; + stream->zalloc = zlib_dynamic_alloc; + stream->zfree = zlib_dynamic_free; + stream->opaque = nullptr; + + if (encoding < 0) { + encoding += 15 - window; + } else { + encoding -= 15 - window; + } + + if (auto err{deflateInit2(stream, level, Z_DEFLATED, encoding, memory, strategy)}; err != Z_OK) { + kphp::log::warning("zlib error {}", zError(err)); + context.destroy(); + return {}; + } + return context; +} + +Optional f$deflate_add(const class_instance& context, const string& data, int64_t flush_type) noexcept { + constexpr uint64_t EXTRA_OUT_SIZE{30}; + constexpr uint64_t MIN_OUT_SIZE{64}; + + switch (flush_type) { + case Z_BLOCK: + case Z_NO_FLUSH: + case Z_PARTIAL_FLUSH: + case Z_SYNC_FLUSH: + case Z_FULL_FLUSH: + case Z_FINISH: + break; + default: + kphp::log::warning("flush type should be one of ZLIB_NO_FLUSH, ZLIB_PARTIAL_FLUSH, ZLIB_SYNC_FLUSH, ZLIB_FULL_FLUSH, ZLIB_FINISH, ZLIB_BLOCK, ZLIB_TREES"); + return {}; + } + + z_stream* stream{std::addressof(context.get()->stream)}; + auto out_size{deflateBound(stream, data.size()) + EXTRA_OUT_SIZE}; + out_size = out_size < MIN_OUT_SIZE ? MIN_OUT_SIZE : out_size; + string out{static_cast(out_size), false}; + stream->next_in = const_cast(reinterpret_cast(data.c_str())); + stream->next_out = reinterpret_cast(out.buffer()); + stream->avail_in = data.size(); + stream->avail_out = out_size; + + auto status{Z_OK}; + uint64_t buffer_used{}; + do { + if (stream->avail_out == 0) { + out.reserve_at_least(out_size + MIN_OUT_SIZE); + out_size += MIN_OUT_SIZE; + stream->avail_out = MIN_OUT_SIZE; + stream->next_out = reinterpret_cast(std::next(out.buffer(), buffer_used)); + } + status = deflate(stream, flush_type); + buffer_used = out_size - stream->avail_out; + } while (status == Z_OK && stream->avail_out == 0); + + std::ptrdiff_t len{}; + switch (status) { + case Z_OK: + len = std::distance(reinterpret_cast(out.buffer()), stream->next_out); + out.shrink(len); + return out; + case Z_STREAM_END: + len = std::distance(reinterpret_cast(out.buffer()), stream->next_out); + deflateReset(stream); + out.shrink(len); + return out; + default: + kphp::log::warning("zlib error {}", zError(status)); + return {}; + } +} diff --git a/runtime-light/stdlib/zlib/zlib-functions.h b/runtime-light/stdlib/zlib/zlib-functions.h index 968a141f36..3168e3d4af 100644 --- a/runtime-light/stdlib/zlib/zlib-functions.h +++ b/runtime-light/stdlib/zlib/zlib-functions.h @@ -10,6 +10,7 @@ #include #include "runtime-common/core/runtime-core.h" +#include "runtime-light/stdlib/zlib/deflate-context.h" namespace kphp::zlib { @@ -53,3 +54,7 @@ inline string f$gzdeflate(const string& data, int64_t level = kphp::zlib::MIN_CO inline string f$gzinflate(const string& data) noexcept { return kphp::zlib::decode({data.c_str(), static_cast(data.size())}, kphp::zlib::ENCODING_RAW).value_or(string{}); } + +class_instance f$deflate_init(int64_t encoding, const array& options = {}) noexcept; + +Optional f$deflate_add(const class_instance& context, const string& data, int64_t flush_type = Z_SYNC_FLUSH) noexcept; diff --git a/tests/phpt/pk/016_gzip.php b/tests/phpt/pk/016_gzip.php index 93e7c980a7..9032555ed8 100644 --- a/tests/phpt/pk/016_gzip.php +++ b/tests/phpt/pk/016_gzip.php @@ -1,4 +1,4 @@ -@ok k2_skip +@ok