diff --git a/be/src/vec/data_types/data_type_ipv4.cpp b/be/src/vec/data_types/data_type_ipv4.cpp index 90a88aa6fc7ee9..963a1adf82e05f 100644 --- a/be/src/vec/data_types/data_type_ipv4.cpp +++ b/be/src/vec/data_types/data_type_ipv4.cpp @@ -37,8 +37,9 @@ std::string DataTypeIPv4::to_string(const IColumn& column, size_t row_num) const auto result = check_column_const_set_readability(column, row_num); ColumnPtr ptr = result.first; row_num = result.second; - IPv4 value = assert_cast(*ptr).get_element(row_num); - return convert_ipv4_to_string(value); + IPv4 ipv4_val = assert_cast(*ptr).get_element(row_num); + auto value = IPv4Value(ipv4_val); + return value.to_string(); } void DataTypeIPv4::to_string(const IColumn& column, size_t row_num, BufferWritable& ostr) const { @@ -48,43 +49,15 @@ void DataTypeIPv4::to_string(const IColumn& column, size_t row_num, BufferWritab Status DataTypeIPv4::from_string(ReadBuffer& rb, IColumn* column) const { auto* column_data = static_cast(column); - StringParser::ParseResult result; - IPv4 val = StringParser::string_to_unsigned_int(rb.position(), rb.count(), &result); + IPv4 val = 0; + if (!read_ipv4_text_impl(val, rb)) { + return Status::InvalidArgument("parse ipv4 fail, string: '{}'", + std::string(rb.position(), rb.count()).c_str()); + } column_data->insert_value(val); return Status::OK(); } -std::string DataTypeIPv4::convert_ipv4_to_string(IPv4 ipv4) { - std::stringstream ss; - ss << ((ipv4 >> 24) & 0xFF) << '.' << ((ipv4 >> 16) & 0xFF) << '.' << ((ipv4 >> 8) & 0xFF) - << '.' << (ipv4 & 0xFF); - return ss.str(); -} - -bool DataTypeIPv4::convert_string_to_ipv4(IPv4& x, std::string ipv4) { - const static int IPV4_PARTS_NUM = 4; - IPv4 parts[IPV4_PARTS_NUM]; - int part_index = 0; - std::stringstream ss(ipv4); - std::string part; - StringParser::ParseResult result; - - while (std::getline(ss, part, '.')) { - IPv4 val = StringParser::string_to_unsigned_int(part.data(), part.size(), &result); - if (UNLIKELY(result != StringParser::PARSE_SUCCESS) || val > 255) { - return false; - } - parts[part_index++] = val; - } - - if (part_index != 4) { - return false; - } - - x = (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]; - return true; -} - MutableColumnPtr DataTypeIPv4::create_column() const { return ColumnIPv4::create(); } diff --git a/be/src/vec/data_types/data_type_ipv4.h b/be/src/vec/data_types/data_type_ipv4.h index d2bd3e487c9a87..89ac8e18447cb2 100644 --- a/be/src/vec/data_types/data_type_ipv4.h +++ b/be/src/vec/data_types/data_type_ipv4.h @@ -57,9 +57,6 @@ class DataTypeIPv4 final : public DataTypeNumberBase { void to_string(const IColumn& column, size_t row_num, BufferWritable& ostr) const override; Status from_string(ReadBuffer& rb, IColumn* column) const override; - static std::string convert_ipv4_to_string(IPv4 ipv4); - static bool convert_string_to_ipv4(IPv4& x, std::string ipv4); - Field get_field(const TExprNode& node) const override { return (IPv4)node.ipv4_literal.value; } MutableColumnPtr create_column() const override; diff --git a/be/src/vec/data_types/data_type_ipv6.cpp b/be/src/vec/data_types/data_type_ipv6.cpp index d54a0f48464ba6..78b8e8e07d723c 100755 --- a/be/src/vec/data_types/data_type_ipv6.cpp +++ b/be/src/vec/data_types/data_type_ipv6.cpp @@ -37,8 +37,9 @@ std::string DataTypeIPv6::to_string(const IColumn& column, size_t row_num) const auto result = check_column_const_set_readability(column, row_num); ColumnPtr ptr = result.first; row_num = result.second; - IPv6 value = assert_cast(*ptr).get_element(row_num); - return convert_ipv6_to_string(value); + IPv6 ipv6_val = assert_cast(*ptr).get_element(row_num); + auto value = IPv6Value(ipv6_val); + return value.to_string(); } void DataTypeIPv6::to_string(const IColumn& column, size_t row_num, BufferWritable& ostr) const { @@ -48,23 +49,15 @@ void DataTypeIPv6::to_string(const IColumn& column, size_t row_num, BufferWritab Status DataTypeIPv6::from_string(ReadBuffer& rb, IColumn* column) const { auto* column_data = static_cast(column); - IPv6 value; - if (!convert_string_to_ipv6(value, rb.to_string())) { - throw doris::Exception(doris::ErrorCode::INVALID_ARGUMENT, - "Invalid value: {} for type IPv6", rb.to_string()); + IPv6 val = 0; + if (!read_ipv6_text_impl(val, rb)) { + return Status::InvalidArgument("parse ipv6 fail, string: '{}'", + std::string(rb.position(), rb.count()).c_str()); } - column_data->insert_value(value); + column_data->insert_value(val); return Status::OK(); } -std::string DataTypeIPv6::convert_ipv6_to_string(IPv6 ipv6) { - return IPv6Value::to_string(ipv6); -} - -bool DataTypeIPv6::convert_string_to_ipv6(IPv6& x, std::string ipv6) { - return IPv6Value::from_string(x, ipv6); -} - MutableColumnPtr DataTypeIPv6::create_column() const { return ColumnIPv6::create(); } diff --git a/be/src/vec/data_types/data_type_ipv6.h b/be/src/vec/data_types/data_type_ipv6.h index f849dab98ac158..87236c5592f11b 100755 --- a/be/src/vec/data_types/data_type_ipv6.h +++ b/be/src/vec/data_types/data_type_ipv6.h @@ -56,12 +56,9 @@ class DataTypeIPv6 final : public DataTypeNumberBase { void to_string(const IColumn& column, size_t row_num, BufferWritable& ostr) const override; Status from_string(ReadBuffer& rb, IColumn* column) const override; - static std::string convert_ipv6_to_string(IPv6 ipv6); - static bool convert_string_to_ipv6(IPv6& x, std::string ipv6); - Field get_field(const TExprNode& node) const override { IPv6 value; - if (!convert_string_to_ipv6(value, node.ipv6_literal.value)) { + if (!IPv6Value::from_string(value, node.ipv6_literal.value)) { throw doris::Exception(doris::ErrorCode::INVALID_ARGUMENT, "Invalid value: {} for type IPv6", node.ipv6_literal.value); } diff --git a/be/src/vec/functions/function_ip.cpp b/be/src/vec/functions/function_ip.cpp index 643c69939d3cee..fce33a0b149b88 100644 --- a/be/src/vec/functions/function_ip.cpp +++ b/be/src/vec/functions/function_ip.cpp @@ -32,5 +32,7 @@ void register_function_ip(SimpleFunctionFactory& factory) { "inet_aton"); factory.register_function(); factory.register_alias(FunctionIPv6NumToString::name, "inet6_ntoa"); + factory.register_function(); + factory.register_function(); } } // namespace doris::vectorized \ No newline at end of file diff --git a/be/src/vec/functions/function_ip.h b/be/src/vec/functions/function_ip.h index 4a39242e85e9e7..2b12ecb713ec1d 100644 --- a/be/src/vec/functions/function_ip.h +++ b/be/src/vec/functions/function_ip.h @@ -348,4 +348,108 @@ class FunctionIPv6NumToString : public IFunction { } }; +class FunctionIsIPv4String : public IFunction { +private: + Status execute_type(Block& block, const ColumnWithTypeAndName& argument, size_t result) const { + const ColumnPtr& column = argument.column; + + if (const auto* col_src = typeid_cast(column.get())) { + auto col_res = ColumnInt8::create(); + + for (size_t i = 0; i < col_src->size(); ++i) { + auto ipv4_str = col_src->get_data_at(i).to_string(); + if (ipv4_str.size() > IPV4_MAX_TEXT_LENGTH || !IPv4Value::is_valid_string(ipv4_str)) { + col_res->insert_value(0); + } else { + col_res->insert_value(1); + } + } + + DCHECK_EQ(col_res->size(), col_src->size()); + + block.replace_by_position( + result, ColumnNullable::create(std::move(col_res), ColumnUInt8::create(col_src->size(), 0))); + return Status::OK(); + } + + return Status::RuntimeError("Illegal column {} of argument of function {}", + argument.column->get_name(), get_name()); + } + +public: + static constexpr auto name = "isipv4string"; + static FunctionPtr create() { + return std::make_shared(); + } + + String get_name() const override { return name; } + + size_t get_number_of_arguments() const override { return 1; } + + DataTypePtr get_return_type_impl(const DataTypes& arguments) const override { + return make_nullable(std::make_shared()); + } + + bool use_default_implementation_for_nulls() const override { return true; } + + Status execute_impl(FunctionContext* context, Block& block, const ColumnNumbers& arguments, + size_t result, size_t input_rows_count) const override { + ColumnWithTypeAndName& argument = block.get_by_position(arguments[0]); + DCHECK(argument.type->get_type_id() == TypeIndex::String); + return execute_type(block, argument, result); + } +}; + +class FunctionIsIPv6String : public IFunction { +private: + Status execute_type(Block& block, const ColumnWithTypeAndName& argument, size_t result) const { + const ColumnPtr& column = argument.column; + + if (const auto* col_src = typeid_cast(column.get())) { + auto col_res = ColumnInt8::create(); + + for (size_t i = 0; i < col_src->size(); ++i) { + auto ipv6_str = col_src->get_data_at(i).to_string(); + if (ipv6_str.size() > IPV6_MAX_TEXT_LENGTH || !IPv6Value::is_valid_string(ipv6_str)) { + col_res->insert_value(0); + } else { + col_res->insert_value(1); + } + } + + DCHECK_EQ(col_res->size(), col_src->size()); + + block.replace_by_position( + result, ColumnNullable::create(std::move(col_res), ColumnUInt8::create(col_src->size(), 0))); + return Status::OK(); + } + + return Status::RuntimeError("Illegal column {} of argument of function {}", + argument.column->get_name(), get_name()); + } + +public: + static constexpr auto name = "isipv6string"; + static FunctionPtr create() { + return std::make_shared(); + } + + String get_name() const override { return name; } + + size_t get_number_of_arguments() const override { return 1; } + + DataTypePtr get_return_type_impl(const DataTypes& arguments) const override { + return make_nullable(std::make_shared()); + } + + bool use_default_implementation_for_nulls() const override { return true; } + + Status execute_impl(FunctionContext* context, Block& block, const ColumnNumbers& arguments, + size_t result, size_t input_rows_count) const override { + ColumnWithTypeAndName& argument = block.get_by_position(arguments[0]); + DCHECK(argument.type->get_type_id() == TypeIndex::String); + return execute_type(block, argument, result); + } +}; + } // namespace doris::vectorized \ No newline at end of file diff --git a/be/test/vec/data_types/from_string_test.cpp b/be/test/vec/data_types/from_string_test.cpp index bbfb7da92a4b69..3c496dc89234f2 100644 --- a/be/test/vec/data_types/from_string_test.cpp +++ b/be/test/vec/data_types/from_string_test.cpp @@ -283,6 +283,57 @@ TEST(FromStringTest, ScalaWrapperFieldVsDataType) { } } + // ipv4 and ipv6 type + { + typedef std::pair FieldType_RandStr; + std::vector ip_scala_field_types = { + FieldType_RandStr(FieldType::OLAP_FIELD_TYPE_IPV4, "127.0.0.1"), + FieldType_RandStr(FieldType::OLAP_FIELD_TYPE_IPV6, "2405:9800:9800:66::2")}; + for (auto pair : ip_scala_field_types) { + auto type = pair.first; + DataTypePtr data_type_ptr = DataTypeFactory::instance().create_data_type(type, 0, 0); + std::cout << "this type is " << data_type_ptr->get_name() << ": " + << fmt::format("{}", type) << std::endl; + + std::unique_ptr min_wf(WrapperField::create_by_type(type)); + std::unique_ptr max_wf(WrapperField::create_by_type(type)); + std::unique_ptr rand_wf(WrapperField::create_by_type(type)); + + min_wf->set_to_min(); + max_wf->set_to_max(); + static_cast(rand_wf->from_string(pair.second, 0, 0)); + + string min_s = min_wf->to_string(); + string max_s = max_wf->to_string(); + string rand_ip = rand_wf->to_string(); + + ReadBuffer min_rb(min_s.data(), min_s.size()); + ReadBuffer max_rb(max_s.data(), max_s.size()); + ReadBuffer rand_rb(rand_ip.data(), rand_ip.size()); + + auto col = data_type_ptr->create_column(); + Status st = data_type_ptr->from_string(min_rb, col); + EXPECT_EQ(st.ok(), true); + st = data_type_ptr->from_string(max_rb, col); + EXPECT_EQ(st.ok(), true); + st = data_type_ptr->from_string(rand_rb, col); + EXPECT_EQ(st.ok(), true); + + string min_s_d = data_type_ptr->to_string(*col, 0); + string max_s_d = data_type_ptr->to_string(*col, 1); + string rand_s_d = data_type_ptr->to_string(*col, 2); + rtrim(min_s); + rtrim(max_s); + rtrim(rand_ip); + std::cout << "min(" << min_s << ") with data_type_str:" << min_s_d << std::endl; + std::cout << "max(" << max_s << ") with data_type_str:" << max_s_d << std::endl; + std::cout << "rand(" << rand_ip << ") with data_type_str:" << rand_s_d << std::endl; + EXPECT_EQ(min_s, min_s_d); + EXPECT_EQ(max_s, max_s_d); + EXPECT_EQ(rand_ip, rand_s_d); + } + } + // null data type { DataTypePtr data_type_ptr = DataTypeFactory::instance().create_data_type( diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/IsIpv4String.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/IsIpv4String.java new file mode 100644 index 00000000000000..50d149e77d253e --- /dev/null +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/IsIpv4String.java @@ -0,0 +1,67 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.doris.nereids.trees.expressions.functions.scalar; + +import org.apache.doris.catalog.FunctionSignature; +import org.apache.doris.nereids.trees.expressions.Expression; +import org.apache.doris.nereids.trees.expressions.functions.AlwaysNullable; +import org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature; +import org.apache.doris.nereids.trees.expressions.shape.BinaryExpression; +import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor; +import org.apache.doris.nereids.types.StringType; +import org.apache.doris.nereids.types.TinyIntType; +import org.apache.doris.nereids.types.VarcharType; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +/** + * scalar function IsIpv4String + */ +public class IsIpv4String extends ScalarFunction + implements BinaryExpression, ExplicitlyCastableSignature, AlwaysNullable { + + public static final List SIGNATURES = ImmutableList.of( + FunctionSignature.ret(TinyIntType.INSTANCE).args(VarcharType.SYSTEM_DEFAULT), + FunctionSignature.ret(TinyIntType.INSTANCE).args(StringType.INSTANCE)); + + public IsIpv4String(Expression arg0) { + super("isipv4string", arg0); + } + + @Override + public IsIpv4String withChildren(List children) { + Preconditions.checkArgument(children.size() == 1, + "isipv4string accept 1 args, but got %s (%s)", + children.size(), + children); + return new IsIpv4String(children.get(0)); + } + + @Override + public List getSignatures() { + return SIGNATURES; + } + + @Override + public R accept(ExpressionVisitor visitor, C context) { + return visitor.visitIsIpv4String(this, context); + } +} diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/IsIpv6String.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/IsIpv6String.java new file mode 100644 index 00000000000000..42609a4ad5a20c --- /dev/null +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/IsIpv6String.java @@ -0,0 +1,67 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.apache.doris.nereids.trees.expressions.functions.scalar; + +import org.apache.doris.catalog.FunctionSignature; +import org.apache.doris.nereids.trees.expressions.Expression; +import org.apache.doris.nereids.trees.expressions.functions.AlwaysNullable; +import org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature; +import org.apache.doris.nereids.trees.expressions.shape.BinaryExpression; +import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor; +import org.apache.doris.nereids.types.StringType; +import org.apache.doris.nereids.types.TinyIntType; +import org.apache.doris.nereids.types.VarcharType; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; + +import java.util.List; + +/** + * scalar function IsIpv6String + */ +public class IsIpv6String extends ScalarFunction + implements BinaryExpression, ExplicitlyCastableSignature, AlwaysNullable { + + public static final List SIGNATURES = ImmutableList.of( + FunctionSignature.ret(TinyIntType.INSTANCE).args(VarcharType.SYSTEM_DEFAULT), + FunctionSignature.ret(TinyIntType.INSTANCE).args(StringType.INSTANCE)); + + public IsIpv6String(Expression arg0) { + super("isipv6string", arg0); + } + + @Override + public IsIpv6String withChildren(List children) { + Preconditions.checkArgument(children.size() == 1, + "isipv6string accept 1 args, but got %s (%s)", + children.size(), + children); + return new IsIpv6String(children.get(0)); + } + + @Override + public List getSignatures() { + return SIGNATURES; + } + + @Override + public R accept(ExpressionVisitor visitor, C context) { + return visitor.visitIsIpv6String(this, context); + } +} diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/ScalarFunctionVisitor.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/ScalarFunctionVisitor.java index ae147b633bd9a0..ccf548eb6da3b7 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/ScalarFunctionVisitor.java +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/ScalarFunctionVisitor.java @@ -191,6 +191,8 @@ import org.apache.doris.nereids.trees.expressions.functions.scalar.Ipv4StringToNumOrDefault; import org.apache.doris.nereids.trees.expressions.functions.scalar.Ipv4StringToNumOrNull; import org.apache.doris.nereids.trees.expressions.functions.scalar.Ipv6NumToString; +import org.apache.doris.nereids.trees.expressions.functions.scalar.IsIpv4String; +import org.apache.doris.nereids.trees.expressions.functions.scalar.IsIpv6String; import org.apache.doris.nereids.trees.expressions.functions.scalar.JsonArray; import org.apache.doris.nereids.trees.expressions.functions.scalar.JsonContains; import org.apache.doris.nereids.trees.expressions.functions.scalar.JsonExtract; @@ -1121,6 +1123,14 @@ default R visitIpv6NumToString(Ipv6NumToString ipv6NumToString, C context) { return visitScalarFunction(ipv6NumToString, context); } + default R visitIsIpv4String(IsIpv4String isIpv4String, C context) { + return visitScalarFunction(isIpv4String, context); + } + + default R visitIsIpv6String(IsIpv6String isIpv6String, C context) { + return visitScalarFunction(isIpv6String, context); + } + default R visitJsonArray(JsonArray jsonArray, C context) { return visitScalarFunction(jsonArray, context); } diff --git a/gensrc/script/doris_builtins_functions.py b/gensrc/script/doris_builtins_functions.py index 91166a6dfb3ca6..b5d6b6dae806aa 100644 --- a/gensrc/script/doris_builtins_functions.py +++ b/gensrc/script/doris_builtins_functions.py @@ -2012,7 +2012,11 @@ [['ipv4stringtonumornull'], 'BIGINT', ['VARCHAR'], 'ALWAYS_NULLABLE'], [['ipv4stringtonumornull'], 'BIGINT', ['STRING'], 'ALWAYS_NULLABLE'], [['ipv6numtostring','inet6_ntoa'], 'VARCHAR', ['VARCHAR'], 'ALWAYS_NULLABLE'], - [['ipv6numtostring','inet6_ntoa'], 'STRING', ['STRING'], 'ALWAYS_NULLABLE'], + [['ipv6numtostring','inet6_ntoa'], 'STRING', ['STRING'], 'ALWAYS_NULLABLE'], + [['isipv4string'], 'TINYINT', ['VARCHAR'], 'ALWAYS_NULLABLE'], + [['isipv4string'], 'TINYINT', ['STRING'], 'ALWAYS_NULLABLE'], + [['isipv6string'], 'TINYINT', ['VARCHAR'], 'ALWAYS_NULLABLE'], + [['isipv6string'], 'TINYINT', ['STRING'], 'ALWAYS_NULLABLE'], ], "NonNullalbe": [ diff --git a/regression-test/data/nereids_function_p0/ip_functions.out b/regression-test/data/nereids_function_p0/ip_functions.out index f906ee221f056f..5ec5b3b1777a6b 100644 --- a/regression-test/data/nereids_function_p0/ip_functions.out +++ b/regression-test/data/nereids_function_p0/ip_functions.out @@ -87,4 +87,16 @@ aaaa:aaaa:ffff:ffff:ffff:ffff:aaaa:aaaa \N -- !ip30 -- -::ffff:127.0.0.1 \ No newline at end of file +::ffff:127.0.0.1 + +-- !ip31 -- +1 + +-- !ip32 -- +0 + +-- !ip33 -- +1 + +-- !ip34 -- +0 \ No newline at end of file diff --git a/regression-test/suites/nereids_function_p0/ip_functions.groovy b/regression-test/suites/nereids_function_p0/ip_functions.groovy index 2a6f8caad6cb79..cd97c01144ba76 100644 --- a/regression-test/suites/nereids_function_p0/ip_functions.groovy +++ b/regression-test/suites/nereids_function_p0/ip_functions.groovy @@ -50,4 +50,9 @@ suite("ip_functions") { qt_ip28 "SELECT inet6_ntoa(unhex('aaaa@#'));" qt_ip29 "SELECT inet6_ntoa(unhex('\0'));" qt_ip30 "SELECT inet6_ntoa(unhex('00000000000000000000FFFF7F000001'));" + + qt_ip31 "SELECT isipv4string('255.255.255.255');" + qt_ip32 "SELECT isipv4string('255.255.255.256');" + qt_ip33 "SELECT isipv6string('2001:5b0:23ff:fffa::113');" + qt_ip34 "SELECT isipv6string('2001:da8:e000:1691:2eaa:7eff:ffe7:7924e');" } \ No newline at end of file