diff --git a/builtin-functions/kphp-light/stdlib/file-functions.txt b/builtin-functions/kphp-light/stdlib/file-functions.txt index 334aec33d2..b3ee9e6737 100644 --- a/builtin-functions/kphp-light/stdlib/file-functions.txt +++ b/builtin-functions/kphp-light/stdlib/file-functions.txt @@ -102,3 +102,6 @@ function stream_context_create ($options ::: mixed = array()) ::: mixed; function fprintf ($stream, $format ::: string, ...$args) ::: int; /** @kphp-extern-func-info stub */ function fputcsv ($stream, $fields ::: array, $delimiter = ",", $enclosure = "\"", $escape = "\\") ::: int | false; + +/** @kphp-extern-func-info stub generation-required */ +function md5_file ($s ::: string, $raw_output ::: bool = false) ::: string | false; diff --git a/builtin-functions/kphp-light/stdlib/zlib-functions.txt b/builtin-functions/kphp-light/stdlib/zlib-functions.txt index c340c48be7..953472780d 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,22 @@ define('ZLIB_RLE', 3); define('ZLIB_FIXED', 4); define('ZLIB_DEFAULT_STRATEGY', 0); -/** @kphp-generate-stub-class */ -final class DeflateContext { - /** @kphp-extern-func-info stub generation-required */ - private function __construct() ::: DeflateContext; -} +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; -// 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; +final class DeflateContext { + private function __construct() ::: DeflateContext; +} diff --git a/runtime-light/stdlib/zlib/deflate-context.h b/runtime-light/stdlib/zlib/deflate-context.h new file mode 100644 index 0000000000..f3ac736a05 --- /dev/null +++ b/runtime-light/stdlib/zlib/deflate-context.h @@ -0,0 +1,63 @@ +// 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 "zlib/zlib.h" + +#include "runtime-common/core/allocator/script-malloc-interface.h" +#include "runtime-common/core/class-instance/refcountable-php-classes.h" +#include "runtime-common/stdlib/visitors/dummy-visitor-methods.h" +#include "runtime-light/stdlib/diagnostics/logs.h" + +class C$DeflateContext : public refcountable_php_classes, private DummyVisitorMethods { + std::optional m_stream; + + static voidpf dynamic_calloc([[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 calloc: can't allocate {} bytes", items * size); + } + return mem; + } + + static void dynamic_free([[maybe_unused]] voidpf opaque, voidpf address) noexcept { + kphp::memory::script::free(address); + } + +public: + using DummyVisitorMethods::accept; + C$DeflateContext() noexcept = default; + + C$DeflateContext(const C$DeflateContext&) = delete; + C$DeflateContext(C$DeflateContext&&) = delete; + C$DeflateContext& operator=(const C$DeflateContext&) = delete; + C$DeflateContext& operator=(C$DeflateContext&&) = delete; + + bool init(int32_t level, int32_t window_bits, int32_t memory, int32_t strategy) noexcept { + z_stream* stream{std::addressof(m_stream.emplace(z_stream{.zalloc = dynamic_calloc, .zfree = dynamic_free, .opaque = nullptr}))}; + + if (auto err{deflateInit2(stream, level, Z_DEFLATED, window_bits, memory, strategy)}; err != Z_OK) { + kphp::log::warning("zlib error {}", err); + return false; + } + + return true; + } + + z_stream& operator*() noexcept { + kphp::log::assertion(m_stream.has_value()); + return *m_stream; + } + + ~C$DeflateContext() { + if (m_stream.has_value()) [[likely]] { + deflateEnd(std::addressof(*m_stream)); + } + } +}; diff --git a/runtime-light/stdlib/zlib/zlib-functions.cpp b/runtime-light/stdlib/zlib/zlib-functions.cpp index d0783e0bee..5798b577ed 100644 --- a/runtime-light/stdlib/zlib/zlib-functions.cpp +++ b/runtime-light/stdlib/zlib/zlib-functions.cpp @@ -73,8 +73,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 +103,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 +113,146 @@ 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 { + static constexpr int32_t DEFAULT_MEMORY{8}; + static constexpr int32_t DEFAULT_WINDOW{15}; + + if (encoding != kphp::zlib::ENCODING_RAW && encoding != kphp::zlib::ENCODING_DEFLATE && encoding != kphp::zlib::ENCODING_GZIP) { + kphp::log::warning("encoding should be one of ZLIB_ENCODING_RAW, ZLIB_ENCODING_DEFLATE, ZLIB_ENCODING_GZIP"); + return {}; + } + + int32_t level{Z_DEFAULT_COMPRESSION}; + int32_t memory{DEFAULT_MEMORY}; + int32_t window{DEFAULT_WINDOW}; + int32_t strategy{Z_DEFAULT_STRATEGY}; + + for (const auto& it : options) { + using namespace std::literals; + + if (!it.is_string_key()) { + kphp::log::warning("unsupported option"); + return {}; + } + + const auto& key{it.get_string_key()}; + const std::string_view key_view{key.c_str(), key.size()}; + const auto& val{it.get_value()}; + + if (key_view == "level"sv) { + if (!val.is_int() || val.as_int() < kphp::zlib::MIN_COMPRESSION_LEVEL || val.as_int() > kphp::zlib::MAX_COMPRESSION_LEVEL) { + kphp::log::warning("option level should be a number between -1..9"); + return {}; + } + level = val.as_int(); + } else if (key_view == "memory"sv) { + if (!val.is_int() || val.as_int() < kphp::zlib::MIN_MEMORY_LEVEL || val.as_int() > kphp::zlib::MAX_MEMORY_LEVEL) { + kphp::log::warning("option memory should be a number between 1..9"); + return {}; + } + memory = val.as_int(); + } else if (key_view == "window"sv) { + if (!val.is_int() || val.as_int() < kphp::zlib::MIN_WINDOW_SIZE || val.as_int() > kphp::zlib::MAX_WINDOW_SIZE) { + kphp::log::warning("option window should be a number between 8..15"); + return {}; + } + window = val.as_int(); + } else if (key_view == "strategy"sv) { + if (!val.is_int()) { + kphp::log::warning("option strategy should be one of ZLIB_FILTERED, ZLIB_HUFFMAN_ONLY, ZLIB_RLE, ZLIB_FIXED or ZLIB_DEFAULT_STRATEGY"); + return {}; + } + const auto s{val.as_int()}; + if (s != Z_DEFAULT_STRATEGY && s != Z_FILTERED && s != Z_HUFFMAN_ONLY && s != Z_RLE && s != Z_FIXED) { + kphp::log::warning("option strategy should be one of ZLIB_FILTERED, ZLIB_HUFFMAN_ONLY, ZLIB_RLE, ZLIB_FIXED or ZLIB_DEFAULT_STRATEGY"); + return {}; + } + strategy = s; + } else if (key_view == "dictionary"sv) { + kphp::log::warning("option dictionary isn't supported yet"); + return {}; + } else { + kphp::log::warning("unknown option name \"{}\"", key.c_str()); + return {}; + } + } + + const int32_t window_bits{[encoding, window] noexcept { + switch (encoding) { + case kphp::zlib::ENCODING_GZIP: + return 16 + window; + case kphp::zlib::ENCODING_RAW: + return -window; + case kphp::zlib::ENCODING_DEFLATE: + return window; + default: + kphp::log::error("deflate_init: unknown encoding {}", encoding); + } + }()}; + + auto context{make_instance()}; + + if (!context->init(level, window_bits, memory, strategy)) { + return {}; + } + return context; +} + +Optional f$deflate_add(const class_instance& context, const string& data, int64_t flush_type) noexcept { + static constexpr uint64_t EXTRA_OUT_SIZE{30}; + static 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())}; + 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); + + switch (status) { + case Z_OK: { + auto len{std::distance(reinterpret_cast(out.buffer()), stream->next_out)}; + out.shrink(len); + return out; + } + case Z_STREAM_END: { + auto len{std::distance(reinterpret_cast(out.buffer()), stream->next_out)}; + deflateReset(stream); + out.shrink(len); + return out; + } + default: { + kphp::log::warning("zlib error {}", status); + return {}; + } + } +} diff --git a/runtime-light/stdlib/zlib/zlib-functions.h b/runtime-light/stdlib/zlib/zlib-functions.h index 968a141f36..0da9ef7e16 100644 --- a/runtime-light/stdlib/zlib/zlib-functions.h +++ b/runtime-light/stdlib/zlib/zlib-functions.h @@ -9,17 +9,26 @@ #include #include +#include "zlib/zlib.h" + #include "runtime-common/core/runtime-core.h" +#include "runtime-light/stdlib/zlib/deflate-context.h" namespace kphp::zlib { -inline constexpr int64_t ENCODING_RAW = -0x0f; -inline constexpr int64_t ENCODING_DEFLATE = 0x0f; -inline constexpr int64_t ENCODING_GZIP = 0x1f; +inline constexpr int64_t ENCODING_RAW{-0x0f}; +inline constexpr int64_t ENCODING_DEFLATE{0x0f}; +inline constexpr int64_t ENCODING_GZIP{0x1f}; + +inline constexpr int64_t MIN_COMPRESSION_LEVEL{-1}; +inline constexpr int64_t MAX_COMPRESSION_LEVEL{9}; +inline constexpr int64_t DEFAULT_COMPRESSION_LEVEL{6}; + +inline constexpr int64_t MIN_MEMORY_LEVEL{1}; +inline constexpr int64_t MAX_MEMORY_LEVEL{9}; -inline constexpr int64_t MIN_COMPRESSION_LEVEL = -1; -inline constexpr int64_t MAX_COMPRESSION_LEVEL = 9; -inline constexpr int64_t DEFAULT_COMPRESSION_LEVEL = 6; +inline constexpr int64_t MIN_WINDOW_SIZE{8}; +inline constexpr int64_t MAX_WINDOW_SIZE{15}; std::optional encode(std::span data, int64_t level, int64_t encoding) noexcept; @@ -53,3 +62,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