Commit 930b4b09 authored by Jasper Chapman-Black's avatar Jasper Chapman-Black Committed by Commit Bot

SuperSize: Caspian: Add native symbol parsing

Bug: 1011921
Change-Id: Ica2e5a7a9222d80a81bd70299219f82798c19855
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1901427
Commit-Queue: Jasper Chapman-Black <jaspercb@chromium.org>
Reviewed-by: default avatarSamuel Huang <huangs@chromium.org>
Cr-Commit-Position: refs/heads/master@{#713842}
parent 108c8869
......@@ -39,6 +39,7 @@ test("caspian_unittests") {
":caspian-lib",
"//testing/gtest",
"//testing/gtest:gtest_main",
"//third_party/re2:re2",
]
}
......
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file
// found in the LICENSE file.
// Much of this logic is duplicated at
// tools/binary_size/libsupersize/function_signature.py.
#include <stddef.h>
#include <algorithm>
#include <deque>
#include <iostream>
#include <string>
#include <string_view>
#include <tuple>
......@@ -15,6 +17,20 @@
#include "tools/binary_size/libsupersize/caspian/function_signature.h"
namespace {
bool EndsWith(std::string_view str,
std::string_view suffix,
size_t pos = std::string_view::npos) {
pos = std::min(pos, str.size());
size_t span = suffix.size();
return pos >= span && str.substr(pos - span, span) == suffix;
}
std::string_view Slice(std::string_view sv, size_t lo, size_t hi) {
return sv.substr(lo, hi - lo);
}
} // namespace
namespace caspian {
std::vector<std::string_view> SplitBy(std::string_view str, char delim) {
std::vector<std::string_view> ret;
......@@ -32,6 +48,8 @@ std::vector<std::string_view> SplitBy(std::string_view str, char delim) {
std::tuple<std::string_view, std::string_view, std::string_view> ParseJava(
std::string_view full_name,
std::deque<std::string>* owned_strings) {
// |owned_strings| is used as an allocator, the relative order of its
// elements can be arbitrary.
std::string maybe_member_type;
size_t hash_idx = full_name.find('#');
std::string_view full_class_name;
......@@ -42,7 +60,7 @@ std::tuple<std::string_view, std::string_view, std::string_view> ParseJava(
// Format: Class#symbol: type
full_class_name = full_name.substr(0, hash_idx);
size_t colon_idx = full_name.find(':');
member = full_name.substr(hash_idx + 1, colon_idx - hash_idx - 1);
member = Slice(full_name, hash_idx + 1, colon_idx);
if (colon_idx != std::string_view::npos) {
member_type = full_name.substr(colon_idx);
}
......@@ -81,4 +99,232 @@ std::tuple<std::string_view, std::string_view, std::string_view> ParseJava(
return std::make_tuple(full_name, template_name, name);
}
size_t FindLastCharOutsideOfBrackets(std::string_view name,
char target_char,
size_t prev_idx) {
int paren_balance_count = 0;
int angle_balance_count = 0;
std::string_view prefix = name.substr(0, prev_idx);
while (true) {
size_t idx = prefix.rfind(target_char);
if (idx == std::string_view::npos) {
return std::string_view::npos;
}
for (char c : prefix.substr(idx)) {
switch (c) {
case '<':
angle_balance_count++;
break;
case '>':
angle_balance_count--;
break;
case '(':
paren_balance_count++;
break;
case ')':
paren_balance_count--;
break;
}
}
if (angle_balance_count == 0 && paren_balance_count == 0) {
return idx;
}
prefix = prefix.substr(0, idx);
}
}
size_t FindReturnValueSpace(std::string_view name, size_t paren_idx) {
size_t space_idx = paren_idx;
// Special case: const cast operators (see tests).
if (EndsWith(name, " const", paren_idx)) {
space_idx = paren_idx - 6;
}
while (true) {
space_idx = FindLastCharOutsideOfBrackets(name, ' ', space_idx);
// Special cases: "operator new", "operator< <templ>", "operator<< <tmpl>".
// No space is added for operator>><tmpl>.
// Currently does not handle operator->, operator->*
if (std::string_view::npos == space_idx) {
break;
}
if (EndsWith(name, "operator", space_idx)) {
space_idx -= 8;
} else if (EndsWith(name, "operator<", space_idx)) {
space_idx -= 9;
} else if (EndsWith(name, "operator<<", space_idx)) {
space_idx -= 10;
} else {
break;
}
}
return space_idx;
}
std::string StripTemplateArgs(std::string name) {
size_t last_right_idx = std::string::npos;
while (true) {
last_right_idx = name.substr(0, last_right_idx).rfind('>');
if (last_right_idx == std::string_view::npos) {
return name;
}
size_t left_idx =
FindLastCharOutsideOfBrackets(name, '<', last_right_idx + 1);
if (left_idx != std::string_view::npos) {
// Leave in empty <>s to denote that it's a template.
name = std::string(name.substr(0, left_idx + 1)) +
std::string(name.substr(last_right_idx));
last_right_idx = left_idx;
}
}
}
std::string NormalizeTopLevelGccLambda(std::string_view name,
size_t left_paren_idx) {
// cc::{lambda(PaintOp*)#63}::_FUN(cc:PaintOp*)
// -> cc::$lambda#63(cc:PaintOp*)
size_t left_brace_idx = name.find('{');
if (left_brace_idx == std::string_view::npos) {
exit(1);
}
size_t hash_idx = name.find('#', left_brace_idx + 1);
if (hash_idx == std::string_view::npos) {
exit(1);
}
size_t right_brace_idx = name.find('}', hash_idx + 1);
if (right_brace_idx == std::string_view::npos) {
exit(1);
}
std::string_view number = Slice(name, hash_idx + 1, right_brace_idx);
std::string ret;
ret += name.substr(0, left_brace_idx);
ret += "$lambda#";
ret += number;
ret += name.substr(left_paren_idx);
return ret;
}
std::string NormalizeTopLevelClangLambda(std::string_view name,
size_t left_paren_idx) {
// cc::$_21::__invoke(int) -> cc::$lambda#21(int)
size_t dollar_idx = name.find('$');
if (dollar_idx == std::string_view::npos) {
exit(1);
}
size_t colon_idx = name.find(':', dollar_idx + 1);
if (colon_idx == std::string_view::npos) {
exit(1);
}
std::string_view number = Slice(name, dollar_idx + 2, colon_idx);
std::string ret;
ret += name.substr(0, dollar_idx);
ret += "$lambda#";
ret += number;
ret += name.substr(left_paren_idx);
return ret;
}
size_t FindParameterListParen(std::string_view name) {
size_t start_idx = 0;
int angle_balance_count = 0;
int paren_balance_count = 0;
while (true) {
size_t idx = name.find('(', start_idx);
if (idx == std::string_view::npos) {
return std::string_view::npos;
}
for (char c : Slice(name, start_idx, idx)) {
switch (c) {
case '<':
angle_balance_count++;
break;
case '>':
angle_balance_count--;
break;
case '(':
paren_balance_count++;
break;
case ')':
paren_balance_count--;
break;
}
}
size_t operator_offset = Slice(name, start_idx, idx).find("operator<");
if (operator_offset != std::string_view::npos) {
if (name[start_idx + operator_offset + 9] == '<') {
// Handle operator<<, <<=
angle_balance_count -= 2;
} else {
// Handle operator<=
angle_balance_count -= 1;
}
} else {
operator_offset = Slice(name, start_idx, idx).find("operator>");
if (operator_offset != std::string_view::npos) {
if (name[start_idx + operator_offset + 9] == '>') {
// Handle operator>>,>>=
angle_balance_count += 2;
} else {
// Handle operator>=
angle_balance_count += 1;
}
}
}
// Adjust paren
if (angle_balance_count == 0 && paren_balance_count == 0) {
// Special case: skip "(anonymous namespace)".
if (name.substr(idx, 21) == "(anonymous namespace)") {
start_idx = idx + 21;
continue;
}
// Special case: skip "decltype (...)"
// Special case: skip "{lambda(PaintOp*)#63}"
if (idx && name[idx - 1] != ' ' && !EndsWith(name, "{lambda", idx)) {
return idx;
}
}
start_idx = idx + 1;
paren_balance_count++;
}
}
std::tuple<std::string, std::string, std::string> ParseCpp(
const std::string& input_name) {
std::string name = input_name;
size_t left_paren_idx = FindParameterListParen(input_name);
std::string full_name = input_name;
if (left_paren_idx != std::string::npos && left_paren_idx > 0) {
size_t right_paren_idx = name.rfind(')');
if (right_paren_idx <= left_paren_idx) {
std::cerr << "ParseCpp() received bad symbol: " << name << std::endl;
exit(1);
}
size_t space_idx = FindReturnValueSpace(name, left_paren_idx);
std::string name_no_params =
std::string(Slice(name, space_idx + 1, left_paren_idx));
// Special case for top-level lambdas.
if (EndsWith(name_no_params, "}::_FUN")) {
// Don't use |name_no_params| in here since prior _idx will be off if
// there was a return value.
name = NormalizeTopLevelGccLambda(name, left_paren_idx);
return ParseCpp(name);
} else if (EndsWith(name_no_params, "::__invoke") &&
name_no_params.find('$') != std::string::npos) {
name = NormalizeTopLevelClangLambda(name, left_paren_idx);
return ParseCpp(name);
}
full_name = name.substr(space_idx + 1);
name = name_no_params + name.substr(right_paren_idx + 1);
}
std::string template_name = name;
name = StripTemplateArgs(name);
return std::make_tuple(full_name, template_name, name);
}
} // namespace caspian
......@@ -23,6 +23,37 @@ std::vector<std::string_view> SplitBy(std::string_view str, char delim);
std::tuple<std::string_view, std::string_view, std::string_view> ParseJava(
std::string_view full_name,
std::deque<std::string>* owned_strings);
// Strips return type and breaks function signature into parts.
// See unit tests for example signatures.
// Returns:
// A tuple of:
// * name without return type (symbol.full_name),
// * full_name without params (symbol.template_name),
// * full_name without params and template args (symbol.name)
std::tuple<std::string, std::string, std::string> ParseCpp(
const std::string& name);
// Returns the last index of |target_char| that is not within ()s nor <>s.
size_t FindLastCharOutsideOfBrackets(std::string_view name,
char target_char,
size_t prev_idx = std::string::npos);
// Finds index of the "(" that denotes the start of a parameter list.
size_t FindParameterListParen(std::string_view name);
// Returns the index of the space that comes after the return type.
size_t FindReturnValueSpace(std::string_view name, size_t paren_idx);
// Different compilers produce different lambda symbols. These utility
// functions standardize the two, so we can compare between compilers.
std::string NormalizeTopLevelGccLambda(std::string_view name,
size_t left_paren_idx);
std::string NormalizeTopLevelClangLambda(std::string_view name,
size_t left_paren_idx);
// Strips the contents of <>, leaving empty <>s to denote that it's a template.
std::string StripTemplateArgs(std::string name);
} // namespace caspian
#endif // TOOLS_BINARY_SIZE_LIBSUPERSIZE_CASPIAN_FUNCTION_SIGNATURE_H_
......@@ -9,6 +9,7 @@
#include <tuple>
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/re2/src/re2/re2.h"
namespace {
std::tuple<std::string, std::string, std::string> PrettyDebug(
......@@ -36,6 +37,59 @@ TEST(AnalyzeTest, StringSplit) {
EXPECT_EQ(expected_output, caspian::SplitBy(input, '/'));
}
TEST(AnalyzeTest, FindLastCharOutsideOfBrackets) {
EXPECT_EQ(caspian::FindLastCharOutsideOfBrackets("(a)a", 'a'), 3u);
EXPECT_EQ(caspian::FindLastCharOutsideOfBrackets("abc(a)a", 'a'), 6u);
EXPECT_EQ(caspian::FindLastCharOutsideOfBrackets("(b)aaa", 'b'),
std::string::npos);
EXPECT_EQ(caspian::FindLastCharOutsideOfBrackets("", 'b'), std::string::npos);
EXPECT_EQ(caspian::FindLastCharOutsideOfBrackets("a(a)a", 'a', 4u), 0u);
EXPECT_EQ(caspian::FindLastCharOutsideOfBrackets("a<<>", '<', 4u), 2u);
}
TEST(AnalyzeTest, FindParameterListParen) {
EXPECT_EQ(caspian::FindParameterListParen("a()"), 1u);
EXPECT_EQ(
caspian::FindParameterListParen(
"bool foo::Bar<unsigned int, int>::Do<unsigned int>(unsigned int)"),
50u);
EXPECT_EQ(caspian::FindParameterListParen(
"std::basic_ostream<char, std::char_traits<char> >& "
"std::operator<< <std::char_traits<char> "
">(std::basic_ostream<char, std::char_traits<char> >&, char)"),
92u);
}
TEST(AnalyzeTest, FindReturnValueSpace) {
EXPECT_EQ(caspian::FindReturnValueSpace("bool a()", 6u), 4u);
EXPECT_EQ(caspian::FindReturnValueSpace("operator delete(void*)", 15),
std::string::npos);
EXPECT_EQ(
caspian::FindReturnValueSpace(
"bool foo::Bar<unsigned int, int>::Do<unsigned int>(unsigned int)",
50u),
4u);
EXPECT_EQ(caspian::FindReturnValueSpace(
"std::basic_ostream<char, std::char_traits<char> >& "
"std::operator<< <std::char_traits<char> "
">(std::basic_ostream<char, std::char_traits<char> >&, char)",
92u),
50u);
}
TEST(AnalyzeTest, NormalizeTopLevelGccLambda) {
EXPECT_EQ(caspian::NormalizeTopLevelGccLambda(
"cc::{lambda(PaintOp*)#63}::_FUN()", 31u),
"cc::$lambda#63()");
}
TEST(AnalyzeTest, NormalizeTopLevelClangLambda) {
// cc::$_21::__invoke() -> cc::$lambda#21()
EXPECT_EQ(caspian::NormalizeTopLevelClangLambda("cc::$_21::__invoke()", 18u),
"cc::$lambda#21()");
}
TEST(AnalyzeTest, ParseJavaFunctionSignature) {
::std::deque<std::string> owned_strings;
// Java method with no args
......@@ -71,4 +125,118 @@ TEST(AnalyzeTest, ParseJavaFunctionSignature) {
do_test("org.ClassName some.Type mField", "org.ClassName#mField: some.Type",
"org.ClassName#mField", "ClassName#mField");
}
TEST(AnalyzeTest, ParseFunctionSignature) {
auto check = [](std::string ret_part, std::string name_part,
std::string params_part, std::string after_part = "",
std::string name_without_templates = "") {
if (name_without_templates.empty()) {
name_without_templates = name_part;
// Heuristic to drop templates: std::vector<int> -> std::vector<>
RE2::GlobalReplace(&name_without_templates, "<.*?>", "<>");
name_without_templates += after_part;
}
std::string signature = name_part + params_part + after_part;
auto result = caspian::ParseCpp(signature);
EXPECT_EQ(name_without_templates, std::get<2>(result));
EXPECT_EQ(name_part + after_part, std::get<1>(result));
EXPECT_EQ(name_part + params_part + after_part, std::get<0>(result));
if (!ret_part.empty()) {
// Parse should be unchanged when we prepend |ret_part|
signature = ret_part + name_part + params_part + after_part;
result = caspian::ParseCpp(signature);
EXPECT_EQ(name_without_templates, std::get<2>(result));
EXPECT_EQ(name_part + after_part, std::get<1>(result));
EXPECT_EQ(name_part + params_part + after_part, std::get<0>(result));
}
};
check("bool ", "foo::Bar<unsigned int, int>::Do<unsigned int>",
"(unsigned int)");
check("base::internal::CheckedNumeric<int>& ",
"base::internal::CheckedNumeric<int>::operator+=<int>", "(int)");
check("base::internal::CheckedNumeric<int>& ",
"b::i::CheckedNumeric<int>::MathOp<b::i::CheckedAddOp, int>", "(int)");
check("", "(anonymous namespace)::GetBridge", "(long long)");
check("", "operator delete", "(void*)");
check("",
"b::i::DstRangeRelationToSrcRangeImpl<long long, long long, "
"std::__ndk1::numeric_limits, (b::i::Integer)1>::Check",
"(long long)");
check("", "cc::LayerIterator::operator cc::LayerIteratorPosition const", "()",
" const");
check("decltype ({parm#1}((SkRecords::NoOp)())) ",
"SkRecord::Record::visit<SkRecords::Draw&>", "(SkRecords::Draw&)",
" const");
check("", "base::internal::BindStateBase::BindStateBase",
"(void (*)(), void (*)(base::internal::BindStateBase const*))");
check("int ", "std::__ndk1::__c11_atomic_load<int>",
"(std::__ndk1::<int> volatile*, std::__ndk1::memory_order)");
check("std::basic_ostream<char, std::char_traits<char> >& ",
"std::operator<< <std::char_traits<char> >",
"(std::basic_ostream<char, std::char_traits<char> >&, char)", "",
"std::operator<< <>");
check("",
"std::basic_istream<char, std::char_traits<char> >"
"::operator>>",
"(unsigned int&)", "", "std::basic_istream<>::operator>>");
check("", "std::operator><std::allocator<char> >", "()", "",
"std::operator><>");
check("", "std::operator>><std::allocator<char> >",
"(std::basic_istream<char, std::char_traits<char> >&)", "",
"std::operator>><>");
check("", "std::basic_istream<char>::operator>", "(unsigned int&)", "",
"std::basic_istream<>::operator>");
check("v8::internal::SlotCallbackResult ",
"v8::internal::UpdateTypedSlotHelper::UpdateCodeTarget"
"<v8::PointerUpdateJobTraits<(v8::Direction)1>::Foo(v8::Heap*, "
"v8::MemoryChunk*)::{lambda(v8::SlotType, unsigned char*)#2}::"
"operator()(v8::SlotType, unsigned char*, unsigned char*) "
"const::{lambda(v8::Object**)#1}>",
"(v8::RelocInfo, v8::Foo<(v8::PointerDirection)1>::Bar(v8::Heap*)::"
"{lambda(v8::SlotType)#2}::operator()(v8::SlotType) const::"
"{lambda(v8::Object**)#1})",
"", "v8::internal::UpdateTypedSlotHelper::UpdateCodeTarget<>");
check("", "WTF::StringAppend<WTF::String, WTF::String>::operator WTF::String",
"()", " const");
// Make sure []s are not removed from the name part.
check("", "Foo", "()", " [virtual thunk]");
// Template function that accepts an anonymous lambda.
check("",
"blink::FrameView::ForAllNonThrottledFrameViews<blink::FrameView::Pre"
"Paint()::{lambda(FrameView&)#2}>",
"(blink::FrameView::PrePaint()::{lambda(FrameView&)#2} const&)", "");
// Test with multiple template args.
check("int ", "Foo<int()>::bar<a<b> >", "()", "", "Foo<>::bar<>");
// See function_signature_test.py for full comment
std::string sig =
"(anonymous namespace)::Foo::Baz() const::GLSLFP::onData(Foo, Bar)";
auto ret = caspian::ParseCpp(sig);
EXPECT_EQ("(anonymous namespace)::Foo::Baz", std::get<2>(ret));
EXPECT_EQ("(anonymous namespace)::Foo::Baz", std::get<1>(ret));
EXPECT_EQ(sig, std::get<0>(ret));
// Top-level lambda.
// Note: Inline lambdas do not seem to be broken into their own symbols.
sig = "cc::{lambda(cc::PaintOp*)#63}::_FUN(cc::PaintOp*)";
ret = caspian::ParseCpp(sig);
EXPECT_EQ("cc::$lambda#63", std::get<2>(ret));
EXPECT_EQ("cc::$lambda#63", std::get<1>(ret));
EXPECT_EQ("cc::$lambda#63(cc::PaintOp*)", std::get<0>(ret));
sig = "cc::$_63::__invoke(cc::PaintOp*)";
ret = caspian::ParseCpp(sig);
EXPECT_EQ("cc::$lambda#63", std::get<2>(ret));
EXPECT_EQ("cc::$lambda#63", std::get<1>(ret));
EXPECT_EQ("cc::$lambda#63(cc::PaintOp*)", std::get<0>(ret));
// Data members
check("", "blink::CSSValueKeywordsHash::findValueImpl", "(char const*)",
"::value_word_list");
check("", "foo::Bar<Z<Y> >::foo<bar>", "(abc)", "::var<baz>",
"foo::Bar<>::foo<>::var<>");
}
} // namespace
......@@ -14,43 +14,42 @@ def _FindParameterListParen(name):
# is necessary (rather than reusing _FindLastCharOutsideOfBrackets), is
# to capture the outer-most function in the case where classes are nested.
start_idx = 0
template_balance_count = 0
paren_balance_count = 0
while True:
template_balance_count = 0
paren_balance_count = 0
while True:
idx = name.find('(', start_idx)
if idx == -1:
return -1
template_balance_count += (
name.count('<', start_idx, idx) - name.count('>', start_idx, idx))
# Special: operators with angle brackets.
operator_idx = name.find('operator<', start_idx, idx)
idx = name.find('(', start_idx)
if idx == -1:
return -1
template_balance_count += (
name.count('<', start_idx, idx) - name.count('>', start_idx, idx))
# Special: operators with angle brackets.
operator_idx = name.find('operator<', start_idx, idx)
if operator_idx != -1:
if name[operator_idx + 9] == '<':
template_balance_count -= 2
else:
template_balance_count -= 1
else:
operator_idx = name.find('operator>', start_idx, idx)
if operator_idx != -1:
if name[operator_idx + 9] == '<':
template_balance_count -= 2
if name[operator_idx + 9] == '>':
template_balance_count += 2
else:
template_balance_count -= 1
else:
operator_idx = name.find('operator>', start_idx, idx)
if operator_idx != -1:
if name[operator_idx + 9] == '>':
template_balance_count += 2
else:
template_balance_count += 1
paren_balance_count += (
name.count('(', start_idx, idx) - name.count(')', start_idx, idx))
if template_balance_count == 0 and paren_balance_count == 0:
# Special case: skip "(anonymous namespace)".
if -1 != name.find('(anonymous namespace)', idx, idx + 21):
start_idx = idx + 21
continue
# Special case: skip "decltype (...)"
# Special case: skip "{lambda(PaintOp*)#63}"
if name[idx - 1] != ' ' and name[idx - 7:idx] != '{lambda':
return idx
start_idx = idx + 1
paren_balance_count += 1
template_balance_count += 1
paren_balance_count += (
name.count('(', start_idx, idx) - name.count(')', start_idx, idx))
if template_balance_count == 0 and paren_balance_count == 0:
# Special case: skip "(anonymous namespace)".
if -1 != name.find('(anonymous namespace)', idx, idx + 21):
start_idx = idx + 21
continue
# Special case: skip "decltype (...)"
# Special case: skip "{lambda(PaintOp*)#63}"
if name[idx - 1] != ' ' and name[idx - 7:idx] != '{lambda':
return idx
start_idx = idx + 1
paren_balance_count += 1
def _FindLastCharOutsideOfBrackets(name, target_char, prev_idx=None):
......@@ -82,12 +81,18 @@ def _FindReturnValueSpace(name, paren_idx):
space_idx = _FindLastCharOutsideOfBrackets(name, ' ', space_idx)
# Special cases: "operator new", "operator< <templ>", "operator<< <tmpl>".
# No space is added for operator>><tmpl>.
if -1 == space_idx or (
-1 == name.find('operator', space_idx - 8, space_idx) and
-1 == name.find('operator<', space_idx - 9, space_idx) and
-1 == name.find('operator<<', space_idx - 10, space_idx)):
if -1 == space_idx:
break
space_idx -= 8
if -1 != name.find('operator', space_idx - 8, space_idx):
space_idx -= 8
elif -1 != name.find('operator<', space_idx - 9, space_idx):
space_idx -= 9
elif -1 != name.find('operator<<', space_idx - 10, space_idx):
space_idx -= 10
else:
break
return space_idx
......@@ -186,9 +191,9 @@ def Parse(name):
assert right_paren_idx > left_paren_idx
space_idx = _FindReturnValueSpace(name, left_paren_idx)
name_no_params = name[space_idx + 1:left_paren_idx]
# Special case for top-level lamdas.
# Special case for top-level lambdas.
if name_no_params.endswith('}::_FUN'):
# Don't use name_no_params in here since prior _idx will be off if
# Don't use |name_no_params| in here since prior _idx will be off if
# there was a return value.
name = _NormalizeTopLevelGccLambda(name, left_paren_idx)
return Parse(name)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment