Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 5 additions & 9 deletions builtin-functions/kphp-light/stdlib/string-functions.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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;

4 changes: 2 additions & 2 deletions runtime-light/runtime-light.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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}")

# =================================================================================================

Expand Down
3 changes: 2 additions & 1 deletion runtime-light/stdlib/stdlib.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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)
139 changes: 139 additions & 0 deletions runtime-light/stdlib/zstd/zstd-functions.cpp
Original file line number Diff line number Diff line change
@@ -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 <cstddef>
#include <cstdint>
#include <optional>
#include <span>

#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<string> compress(std::span<const std::byte> data, int64_t level, std::span<const std::byte> 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<int>(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<char*>(out.dst), out.pos);
out.pos = 0;
} while (result);
return encoded_string;
}

std::optional<string> uncompress(std::span<const std::byte> data, std::span<const std::byte> 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<string::size_type>(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<char*>(out.dst), static_cast<string::size_type>(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<char*>(out.dst), static_cast<string::size_type>(out.pos));
return decoded_string;
}

} // namespace kphp::zstd
57 changes: 57 additions & 0 deletions runtime-light/stdlib/zstd/zstd-functions.h
Original file line number Diff line number Diff line change
@@ -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 <cstddef>
#include <cstdint>
#include <optional>
#include <span>

#include "runtime-common/core/runtime-core.h"

namespace kphp::zstd {

inline constexpr int64_t DEFAULT_COMPRESS_LEVEL = 3;

std::optional<string> compress(std::span<const std::byte> data, int64_t level = DEFAULT_COMPRESS_LEVEL,
std::span<const std::byte> dict = std::span<const std::byte>{}) noexcept;

std::optional<string> uncompress(std::span<const std::byte> data, std::span<const std::byte> dict = std::span<const std::byte>{}) noexcept;

} // namespace kphp::zstd

inline Optional<string> f$zstd_compress(const string& data, int64_t level = kphp::zstd::DEFAULT_COMPRESS_LEVEL) noexcept {
auto res{kphp::zstd::compress({reinterpret_cast<const std::byte*>(data.c_str()), static_cast<size_t>(data.size())}, level)};
if (!res) [[unlikely]] {
return false;
}
return res.value();
}

inline Optional<string> f$zstd_uncompress(const string& data) noexcept {
auto res{kphp::zstd::uncompress({reinterpret_cast<const std::byte*>(data.c_str()), static_cast<size_t>(data.size())})};
if (!res) [[unlikely]] {
return false;
}
return res.value();
}

inline Optional<string> f$zstd_compress_dict(const string& data, const string& dict) noexcept {
auto res{kphp::zstd::compress({reinterpret_cast<const std::byte*>(data.c_str()), static_cast<size_t>(data.size())}, kphp::zstd::DEFAULT_COMPRESS_LEVEL,
{reinterpret_cast<const std::byte*>(dict.c_str()), static_cast<size_t>(dict.size())})};
if (!res) [[unlikely]] {
return false;
}
return res.value();
}

inline Optional<string> f$zstd_uncompress_dict(const string& data, const string& dict) noexcept {
auto res{kphp::zstd::uncompress({reinterpret_cast<const std::byte*>(data.c_str()), static_cast<size_t>(data.size())},
{reinterpret_cast<const std::byte*>(dict.c_str()), static_cast<size_t>(dict.size())})};
if (!res) [[unlikely]] {
return false;
}
return res.value();
}
2 changes: 1 addition & 1 deletion tests/phpt/zstd/1_compress.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@ok k2_skip
@ok
<?php

function test_compress_levels() {
Expand Down
2 changes: 1 addition & 1 deletion tests/phpt/zstd/2_uncompress.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@ok k2_skip
@ok
<?php

function test_uncompress() {
Expand Down
16 changes: 16 additions & 0 deletions tests/phpt/zstd/4_compress_uncompress_without_large_strings.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
@ok
<?php

require_once 'kphp_tester_include.php';

function test_compress_uncompress() {
var_dump(zstd_uncompress((string)zstd_compress("foo bar baz")));

var_dump(zstd_uncompress((string)zstd_compress(str_repeat("foo bar baz", 10000))));

$random_data = (string)openssl_random_pseudo_bytes(1024*1024*15);
assert_true(zstd_uncompress((string)zstd_compress($random_data)) === $random_data);
}


test_compress_uncompress();
Loading