Skip to content
44 changes: 20 additions & 24 deletions builtin-functions/kphp-light/stdlib/zlib-functions.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
26 changes: 26 additions & 0 deletions runtime-light/stdlib/zlib/deflate-context.h
Original file line number Diff line number Diff line change
@@ -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<C$DeflateContext>, private DummyVisitorMethods {
C$DeflateContext() noexcept = default;
using DummyVisitorMethods::accept;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I'd swap these two lines


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{};
};
168 changes: 164 additions & 4 deletions runtime-light/stdlib/zlib/zlib-functions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please explain the reason you chose calloc instead of alloc?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because old KPHP runtime does

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. Let's then rename it to zlib_dynamic_calloc

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 {
Expand Down Expand Up @@ -73,8 +86,7 @@ std::optional<string> encode(std::span<const char> data, int64_t level, int64_t
zstrm.avail_out = out_size_upper_bound;
zstrm.next_out = reinterpret_cast<Bytef*>(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 {};
}
Expand Down Expand Up @@ -104,9 +116,8 @@ std::optional<string> decode(std::span<const char> 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<Bytef*>(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 {};
}
Expand All @@ -115,3 +126,152 @@ std::optional<string> decode(std::span<const char> data, int64_t encoding) noexc
}

} // namespace kphp::zlib

class_instance<C$DeflateContext> f$deflate_init(int64_t encoding, const array<mixed>& options) noexcept {
int32_t level{-1};
int32_t memory{8};
int32_t window{15};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we not have such magic constants?

auto strategy{Z_DEFAULT_STRATEGY};
constexpr auto extract_int_option{[](int32_t lbound, int32_t ubound, const array_iterator<const mixed>& option, int32_t& dst) noexcept {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to see this refactored as now it's hard to undersand

if (const mixed & value{option.get_value()}; value.is_int() && value.as_int() >= lbound && value.as_int() <= ubound) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: something is wrong with your formatter as it should be const mixed&

dst = value.as_int();
return true;
} else {
kphp::log::warning("option {} should be number between {}..{}", option.get_string_key().c_str(), lbound, ubound);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: a number

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") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can have const std::string_view key_view{...} defined once

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<C$DeflateContext> context;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: there is a nice make_instance function that also allocates a class instance

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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's get rid of all the magic constants

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));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a common pattern here to just print the error number

context.destroy();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it enough to just wait for C$DeflateContext's destructor to be invoked?

return {};
}
return context;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like that we allocate a class_instance<C$DeflateContext> even in the case of error. Can we create a zstream, try to make deflateInit2 on it, and then somehow move the stream?

}

Optional<string> f$deflate_add(const class_instance<C$DeflateContext>& 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<string::size_type>(out_size), false};
stream->next_in = const_cast<Bytef*>(reinterpret_cast<const Bytef*>(data.c_str()));
stream->next_out = reinterpret_cast<Bytef*>(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<Bytef*>(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<Bytef*>(out.buffer()), stream->next_out);
out.shrink(len);
return out;
case Z_STREAM_END:
len = std::distance(reinterpret_cast<Bytef*>(out.buffer()), stream->next_out);
deflateReset(stream);
out.shrink(len);
return out;
default:
kphp::log::warning("zlib error {}", zError(status));
return {};
}
}
5 changes: 5 additions & 0 deletions runtime-light/stdlib/zlib/zlib-functions.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <span>

#include "runtime-common/core/runtime-core.h"
#include "runtime-light/stdlib/zlib/deflate-context.h"

namespace kphp::zlib {

Expand Down Expand Up @@ -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<size_t>(data.size())}, kphp::zlib::ENCODING_RAW).value_or(string{});
}

class_instance<C$DeflateContext> f$deflate_init(int64_t encoding, const array<mixed>& options = {}) noexcept;

Optional<string> f$deflate_add(const class_instance<C$DeflateContext>& context, const string& data, int64_t flush_type = Z_SYNC_FLUSH) noexcept;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing include for zlib here

2 changes: 1 addition & 1 deletion tests/phpt/pk/016_gzip.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@ok k2_skip
@ok
<?php

$x = "";
Expand Down
Loading