diff --git a/builtin-functions/_functions.txt b/builtin-functions/_functions.txt index c93d8403db..315a4e3b1d 100644 --- a/builtin-functions/_functions.txt +++ b/builtin-functions/_functions.txt @@ -179,6 +179,13 @@ function kphp_backtrace($pretty ::: bool = true) ::: string[]; function ini_get ($s ::: string) ::: string | false; function ini_set ($s ::: string, $v ::: string) ::: bool; +define('INI_SCANNER_NORMAL', 0); +define('INI_SCANNER_RAW', 1); +define('INI_SCANNER_TYPED', 2); + +function parse_ini_string($ini_string ::: string, $process_sections ::: bool = false, $scanner_mode ::: int = INI_SCANNER_NORMAL) ::: mixed[]; +function parse_ini_file($filename ::: string, $process_sections ::: bool = false, $scanner_mode ::: int = INI_SCANNER_NORMAL) ::: mixed[]; + function memory_get_usage ($real_usage ::: bool = false) ::: int; function memory_get_peak_usage ($real_usage ::: bool = false) ::: int; function memory_get_total_usage() ::: int; diff --git a/runtime/ini.cpp b/runtime/ini.cpp new file mode 100644 index 0000000000..143e20ae37 --- /dev/null +++ b/runtime/ini.cpp @@ -0,0 +1,240 @@ +#include "ini.h" + +/* + * INI parsing functions + */ + +const int64_t SINGLE_QOUTES = 0; +const int64_t DOUBLE_QUOTES = 1; +const int64_t ALL_QUOTES = 2; + +string clear_quotes(const string &str, int64_t flag) { + if (str.empty()) { + return {}; + } + + Optional clear_str; + + switch (flag) { + case SINGLE_QOUTES: + clear_str = f$preg_replace(string("/'+/"), string(""), str); + break; + case DOUBLE_QUOTES: + clear_str = f$preg_replace(string("/\"+/"), string(""), str); + break; + case ALL_QUOTES: + clear_str = f$preg_replace(string("/['|\"]+/"), string(""), str); + break; + } + + if (clear_str.is_null()) { + return string(""); + } + + return clear_str.ref(); +} + +string clear_extra_spaces(const string &str) { + if (str.empty()) { + return {}; + } + + Optional clear_str; + + clear_str = f$preg_replace(string("/ +/"), string(" "), str); + + if (clear_str.is_null()) { + return string(""); + } + + return clear_str.ref(); +} + +/* + * INI parsing functions + */ + +bool is_ini_section(const string &ini_section) { + return f$preg_match(string("/^\\[.+\\]$/"), ini_section).val(); +} + +bool is_ini_var(const string &ini_var) { + return f$preg_match(string("/^.+/"), ini_var).val(); +} + +bool is_ini_val(const string &ini_val) { + return f$preg_match(string("/(.*\n(?=[A-Z])|.*$)/"), ini_val).val(); +} + +bool is_ini_bool_val(const string &ini_val) { + return f$preg_match(string("/^(false|true|0|1|off|on)$/i"), ini_val).val(); +} + +bool is_ini_float_val(const string &ini_val) { + return f$preg_match(string(R"(/^\d+\.\d*$/)"), ini_val).val(); +} + +bool is_ini_str_val(const string &ini_val) { + return f$preg_match(string("/^.*$/i"), ini_val).val(); +} + +string get_ini_section(const string &ini_entry) { + if (ini_entry.empty()) { + return {}; + } + + return ini_entry.substr(1, ini_entry.size()-2); +} + +array split_ini_entry(const string &ini_entry) { + if (ini_entry.empty()) { + return {}; + } + + array res = f$explode(string("="), ini_entry, 2); + + if (res.size().int_size != 2) { + php_warning("Error"); + return {}; + } + + return res; +} + +bool cast_str_to_bool(const string &ini_var) { + if (ini_var.empty()) { + return false; + } + + string ini_bool_var = f$strtolower(ini_var); + + if (ini_bool_var == string("on") || ini_bool_var == string("1") || ini_bool_var == string("true")) { + return true; + } + + if (ini_bool_var == string("off") || ini_bool_var == string("0") || ini_bool_var == string("false")) { + return false; + } + + return false; +} + +array f$parse_ini_string(const string &ini_string, bool process_sections, int scanner_mode) { + if (ini_string.empty()) { + return {}; + } + + string ini_string_copy = f$trim(ini_string); + ini_string_copy = clear_extra_spaces(ini_string_copy); + + array res(array_size(0, 0, true)); + array section(array_size(0, 0, true)); + string ini_entry; + string ini_section; + + for (string::size_type i = 0; i <= ini_string_copy.size(); ++i) { + if (ini_string_copy[i] == '[') { + while (ini_string_copy[i] != ']') { + ini_entry.push_back(ini_string_copy[i]); + ++i; + } + } + + if (ini_string_copy[i] == '\"' || ini_string_copy[i] == '\'') { + ini_entry.push_back(ini_string_copy[i]); + + ++i; + while (ini_string_copy[i] != '\"' && ini_string_copy[i] != '\'') { + ini_entry.push_back(ini_string_copy[i]); + ++i; + } + } + + if (ini_string_copy[i] == ' ' || ini_string_copy[i] == '\n' || ini_string_copy[i] == '\0') { + if (is_ini_section(ini_entry)) { + if (process_sections && !ini_section.empty()) { + res.set_value(ini_section, section); + section.clear(); + } + + ini_section = get_ini_section(ini_entry); + } else if (!ini_entry.empty()){ + array ini = split_ini_entry(ini_entry); + + if (ini.size().int_size != 2) { + php_warning("Invalid ini string format %s", ini_entry.c_str()); + return {}; + } + + string ini_var = ini[0]; + string ini_val = ini[1]; + + if (!is_ini_var(ini_var) && !is_ini_val(ini_val)) { + php_warning("Invalid ini string format %s", ini_entry.c_str()); + return {}; + } + + if (!ini_var.empty()) { + switch (scanner_mode) { + case INI_SCANNER_NORMAL: + if (is_ini_bool_val(ini_val)) { + section.set_value(ini_var, cast_str_to_bool(ini_val) ? string("1") : string("")); + } else if (is_ini_str_val(ini_val)) { + section.set_value(ini_var, clear_quotes(ini_val, ALL_QUOTES)); + } + break; + case INI_SCANNER_RAW: + if (is_ini_str_val(ini_val)) { + section.set_value(ini_var, clear_quotes(ini_val, DOUBLE_QUOTES)); + } else { + section.set_value(ini_var, ini_val); + } + break; + case INI_SCANNER_TYPED: + if (ini_val.is_int()) { + section.set_value(ini_var, ini_val.to_int()); + } else if (is_ini_float_val(ini_val)) { + section.set_value(ini_var, ini_val.to_float()); + } else if (is_ini_bool_val(ini_val)) { + section.set_value(ini_var, cast_str_to_bool(ini_val)); + } else if (is_ini_str_val(ini_val)) { + section.set_value(ini_var, clear_quotes(ini_val, ALL_QUOTES)); + } + break; + } + } + } + + ini_entry = {}; + } else { + ini_entry.push_back(ini_string_copy[i]); + } + } + + if (process_sections) { + if (!ini_section.empty()) { + res.set_value(ini_section, section); + } + } else { + return section; + } + + return res; +} + + + +array f$parse_ini_file(const string &filename, bool process_sections, int scanner_mode) { + if (filename.empty()) { + php_warning("Filename cannot be empty"); + return {}; + } + + Optional ini_string = f$file_get_contents(filename); + + if (ini_string.is_null()) { + return {}; + } + + return f$parse_ini_string(ini_string.ref(), process_sections, scanner_mode); +} diff --git a/runtime/ini.h b/runtime/ini.h new file mode 100644 index 0000000000..09ee6156fc --- /dev/null +++ b/runtime/ini.h @@ -0,0 +1,15 @@ +#pragma once + +#include "runtime/kphp_core.h" +#include "runtime/string_functions.h" +#include "runtime/array_functions.h" +#include "runtime/streams.h" +#include "runtime/regexp.h" + +const int INI_SCANNER_NORMAL = 0; +const int INI_SCANNER_RAW = 1; +const int INI_SCANNER_TYPED = 2; + +array f$parse_ini_string(const string &ini_string, bool process_sections = false, int scanner_mode = 0); + +array f$parse_ini_file(const string &filename, bool process_sections = false, int scanner_mode = 0); diff --git a/runtime/runtime.cmake b/runtime/runtime.cmake index da6beb13c1..4207b1df78 100644 --- a/runtime/runtime.cmake +++ b/runtime/runtime.cmake @@ -83,6 +83,7 @@ prepend(KPHP_RUNTIME_SOURCES ${BASE_DIR}/runtime/ net_events.cpp on_kphp_warning_callback.cpp openssl.cpp + ini.cpp php_assert.cpp profiler.cpp regexp.cpp diff --git a/tests/cpp/runtime/ini-parsing-tests.cpp b/tests/cpp/runtime/ini-parsing-tests.cpp new file mode 100644 index 0000000000..684eb881a9 --- /dev/null +++ b/tests/cpp/runtime/ini-parsing-tests.cpp @@ -0,0 +1,165 @@ +#include "runtime/ini.h" +#include +#include +#include + +TEST(parsing_functions_test, test_parse_ini_string_empty) { + array res = f$parse_ini_string(string("")); + ASSERT_TRUE(res.empty()); +} + +TEST(parsing_functions_test, test_parse_ini_string_scanner_mode_normal) { + array res(array_size(0, 0, true)); + + // Sections false + res = f$parse_ini_string(string("[one] hello=world world=hello [two] a=a b=b")); + ASSERT_EQ(4, res.size().string_size); + + ASSERT_TRUE(res.has_key(string("hello"))); + ASSERT_TRUE(res.has_key(string("world"))); + ASSERT_TRUE(res.has_key(string("a"))); + ASSERT_TRUE(res.has_key(string("b"))); + + res = f$parse_ini_string(string("hello=world world=hello a=a b=b")); + ASSERT_EQ(4, res.size().string_size); + + ASSERT_TRUE(res.has_key(string("hello"))); + ASSERT_TRUE(res.has_key(string("world"))); + ASSERT_TRUE(res.has_key(string("a"))); + ASSERT_TRUE(res.has_key(string("b"))); + + // Sections true + res = f$parse_ini_string(string("[one] [two]"), true); + + ASSERT_EQ(2, res.size().string_size); + ASSERT_TRUE(res.has_key(string("one"))); + ASSERT_TRUE(res.has_key(string("two"))); + + res = f$parse_ini_string(string("[one] hello=world world=hello [two] a=a b=b"), true); + + ASSERT_EQ(2, res.size().string_size); + ASSERT_TRUE(res.has_key(string("one"))); + ASSERT_TRUE(res.get_value(string("one")).is_array()); + ASSERT_TRUE(res.get_value(string("one")).to_array().has_key(string("hello"))); + ASSERT_EQ(res.get_value(string("one")).to_array().get_value(string("hello")).as_string(), string("world")); + ASSERT_TRUE(res.get_value(string("one")).to_array().has_key(string("world"))); + ASSERT_EQ(res.get_value(string("one")).to_array().get_value(string("world")).as_string(), string("hello")); + + ASSERT_TRUE(res.has_key(string("two"))); + ASSERT_TRUE(res.get_value(string("two")).is_array()); + ASSERT_TRUE(res.get_value(string("two")).to_array().has_key(string("a"))); + ASSERT_EQ(res.get_value(string("two")).to_array().get_value(string("a")).as_string(), string("a")); + ASSERT_TRUE(res.get_value(string("two")).to_array().has_key(string("b"))); + ASSERT_EQ(res.get_value(string("two")).to_array().get_value(string("b")).as_string(), string("b")); +} + +TEST(parsing_functions_test, test_parse_ini_string_scanner_mode_raw) { + array res(array_size(0, 0, true)); + + // Sections false + res = f$parse_ini_string(string(R"([one] hello='world' world="hello" [two] a='a' b=Off)"), false, INI_SCANNER_RAW); + + ASSERT_EQ(4, res.size().string_size); + ASSERT_TRUE(res.has_key(string("hello"))); + ASSERT_STREQ(string("'world'").c_str(), res.get_value(string("hello")).to_string().c_str()); + ASSERT_TRUE(res.has_key(string("world"))); + ASSERT_STREQ(string("hello").c_str(), res.get_value(string("world")).to_string().c_str()); + ASSERT_TRUE(res.has_key(string("a"))); + ASSERT_STREQ(string("'a'").c_str(), res.get_value(string("a")).to_string().c_str()); + ASSERT_TRUE(res.has_key(string("b"))); + ASSERT_STREQ(string("Off").c_str(), res.get_value(string("b")).to_string().c_str()); + + // Sections true + res = f$parse_ini_string(string(R"([one] hello='world' world="hello" [two] a='a' b=Off)"), true, INI_SCANNER_RAW); + + ASSERT_EQ(2, res.size().string_size); + ASSERT_TRUE(res.has_key(string("one"))); + ASSERT_TRUE(res.get_value(string("one")).to_array().has_key(string("hello"))); + ASSERT_STREQ(string("'world'").c_str(), res.get_value(string("one")).to_array().get_value(string("hello")).to_string().c_str()); + ASSERT_TRUE(res.get_value(string("one")).to_array().has_key(string("world"))); + ASSERT_STREQ(string("hello").c_str(), res.get_value(string("one")).to_array().get_value(string("world")).to_string().c_str()); + + ASSERT_TRUE(res.has_key(string("two"))); + ASSERT_TRUE(res.get_value(string("two")).to_array().has_key(string("a"))); + ASSERT_STREQ(string("'a'").c_str(), res.get_value(string("two")).to_array().get_value(string("a")).to_string().c_str()); + ASSERT_TRUE(res.get_value(string("two")).to_array().has_key(string("b"))); + ASSERT_STREQ(string("Off").c_str(), res.get_value(string("two")).to_array().get_value(string("b")).to_string().c_str()); +} + +TEST(parsing_functions_test, test_parse_ini_string_scanner_mode_typed) { + array res(array_size(0, 0, true)); + + // Sections false + res = f$parse_ini_string(string("[types vars] int=1500 float=3.14242 str=\"hello world\" bool=true"), false, INI_SCANNER_TYPED); + + ASSERT_EQ(4, res.size().string_size); + + ASSERT_TRUE(res.get_value(string("int")).is_int()); + ASSERT_EQ(1500, res.get_value(string("int")).to_int()); + + ASSERT_TRUE(res.get_value(string("float")).is_float()); + ASSERT_EQ(3.14242, res.get_value(string("float")).to_float()); + + ASSERT_TRUE(res.get_value(string("str")).is_string()); + ASSERT_EQ(string("hello world"), res.get_value(string("str")).to_string()); + + ASSERT_TRUE(res.get_value(string("bool")).is_bool()); + ASSERT_EQ(true, res.get_value(string("bool")).to_bool()); + + // Sections true + res = f$parse_ini_string(string("[types vars] int=1500 float=3.14242 str=\"hello world\" bool1=true bool2=Off"), true, INI_SCANNER_TYPED); + + ASSERT_TRUE(res.has_key(string("types vars"))); + ASSERT_TRUE(res.get_value(string("types vars")).to_array().get_value(string("int")).is_int()); + ASSERT_EQ(1500, res.get_value(string("types vars")).to_array().get_value(string("int")).to_int()); + + ASSERT_TRUE(res.get_value(string("types vars")).to_array().get_value(string("float")).is_float()); + ASSERT_EQ(3.14242, res.get_value(string("types vars")).to_array().get_value(string("float")).to_float()); + + ASSERT_TRUE(res.get_value(string("types vars")).to_array().get_value(string("str")).is_string()); + ASSERT_EQ(string("hello world"), res.get_value(string("types vars")).to_array().get_value(string("str")).to_string()); + + ASSERT_TRUE(res.get_value(string("types vars")).to_array().get_value(string("bool1")).is_bool()); + ASSERT_EQ(true, res.get_value(string("types vars")).to_array().get_value(string("bool1")).to_bool()); + + ASSERT_TRUE(res.get_value(string("types vars")).to_array().get_value(string("bool2")).is_bool()); + ASSERT_EQ(false, res.get_value(string("types vars")).to_array().get_value(string("bool2")).to_bool()); +} + +TEST(parsing_functions_test, test_parse_ini_file) { + std::ofstream of("test.ini"); + + if (of.is_open()) { + of << "[types vars] "<< std::endl; + of << "int=1500" << std::endl; + of << "float=3.14242" << std::endl; + of << "str1=\"hello world\"" << std::endl; + of << "str2=\'hello world\'" << std::endl; + of << "bool1=true" << std::endl; + of << "bool2=Off"; + of.close(); + } + + array res(array_size(0, 0, true)); + + res = f$parse_ini_file(string("test.ini")); + + ASSERT_EQ(6, res.size().string_size); + + res = f$parse_ini_file(string("test.ini"), true); + + ASSERT_EQ(1, res.size().string_size); + + res = f$parse_ini_file(string("test.ini"), false, INI_SCANNER_RAW); + + ASSERT_EQ(6, res.size().string_size); + + res = f$parse_ini_file(string("test.ini"), true, INI_SCANNER_RAW); + + ASSERT_EQ(1, res.size().string_size); + + res = f$parse_ini_file(string("test.ini"), false, INI_SCANNER_TYPED); + + ASSERT_EQ(6, res.size().string_size); +} + diff --git a/tests/cpp/runtime/runtime-tests.cmake b/tests/cpp/runtime/runtime-tests.cmake index 32f74e5d7a..8de07ffd72 100644 --- a/tests/cpp/runtime/runtime-tests.cmake +++ b/tests/cpp/runtime/runtime-tests.cmake @@ -19,7 +19,8 @@ prepend(RUNTIME_TESTS_SOURCES ${BASE_DIR}/tests/cpp/runtime/ memory_resource/unsynchronized_pool_resource-test.cpp string-list-test.cpp string-test.cpp - zstd-test.cpp) + zstd-test.cpp + ini-parsing-tests.cpp) allow_deprecated_declarations_for_apple(${BASE_DIR}/tests/cpp/runtime/inter-process-mutex-test.cpp) vk_add_unittest(runtime "${RUNTIME_LIBS};${RUNTIME_LINK_TEST_LIBS}" ${RUNTIME_TESTS_SOURCES}) diff --git a/tests/phpt/parsing/001_ini_parsing.php b/tests/phpt/parsing/001_ini_parsing.php new file mode 100644 index 0000000000..64947df957 --- /dev/null +++ b/tests/phpt/parsing/001_ini_parsing.php @@ -0,0 +1,89 @@ +@ok +