mirror of
https://github.com/ethereum/solidity
synced 2023-10-03 13:03:40 +00:00
More fixes.
This commit is contained in:
parent
f260b91001
commit
1364253dd9
@ -551,8 +551,12 @@ LinkerObject const& Assembly::assemble() const
|
|||||||
"Cannot push and assign immutables in the same assembly subroutine."
|
"Cannot push and assign immutables in the same assembly subroutine."
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: assert zero inputs/outputs on code section zero
|
assertThrow(!m_codeSections.empty(), AssemblyException, "Expected at least one code section.");
|
||||||
// TODO: assert one code section being present and *only* one being present unless EOF
|
assertThrow(eof || m_codeSections.size() > 1, AssemblyException, "Expected exactly one code section in non-EOF code.");
|
||||||
|
assertThrow(
|
||||||
|
m_codeSections.front().inputs == 0 && m_codeSections.front().outputs == 0, AssemblyException,
|
||||||
|
"Expected the first code section to have zero inputs and outputs."
|
||||||
|
);
|
||||||
|
|
||||||
unsigned bytesRequiredForSubs = 0;
|
unsigned bytesRequiredForSubs = 0;
|
||||||
// TODO: consider fully producing all sub and data refs in this pass already.
|
// TODO: consider fully producing all sub and data refs in this pass already.
|
||||||
@ -569,15 +573,13 @@ LinkerObject const& Assembly::assemble() const
|
|||||||
// Insert EOF1 header.
|
// Insert EOF1 header.
|
||||||
vector<size_t> codeSectionSizeOffsets;
|
vector<size_t> codeSectionSizeOffsets;
|
||||||
auto setCodeSectionSize = [&](size_t _section, size_t _size) {
|
auto setCodeSectionSize = [&](size_t _section, size_t _size) {
|
||||||
bytesRef length(ret.bytecode.data() + codeSectionSizeOffsets.at(_section), 2);
|
toBigEndian(_size, bytesRef(ret.bytecode.data() + codeSectionSizeOffsets.at(_section), 2));
|
||||||
toBigEndian(_size, length);
|
|
||||||
};
|
};
|
||||||
std::optional<size_t> dataSectionSizeOffset;
|
std::optional<size_t> dataSectionSizeOffset;
|
||||||
auto setDataSectionSize = [&](size_t _size) {
|
auto setDataSectionSize = [&](size_t _size) {
|
||||||
assertThrow(dataSectionSizeOffset.has_value(), AssemblyException, "");
|
assertThrow(dataSectionSizeOffset.has_value(), AssemblyException, "");
|
||||||
assertThrow(_size <= 0xFFFF, AssemblyException, "");
|
assertThrow(_size <= 0xFFFF, AssemblyException, "");
|
||||||
bytesRef length(ret.bytecode.data() + *dataSectionSizeOffset, 2);
|
toBigEndian(static_cast<uint16_t>(_size), bytesRef(ret.bytecode.data() + *dataSectionSizeOffset, 2));
|
||||||
toBigEndian(static_cast<uint16_t>(_size), length);
|
|
||||||
};
|
};
|
||||||
if (eof)
|
if (eof)
|
||||||
{
|
{
|
||||||
@ -591,8 +593,7 @@ LinkerObject const& Assembly::assemble() const
|
|||||||
ret.bytecode.push_back(0x03); // kind=type
|
ret.bytecode.push_back(0x03); // kind=type
|
||||||
ret.bytecode.push_back(0x00); // length of type section
|
ret.bytecode.push_back(0x00); // length of type section
|
||||||
ret.bytecode.push_back(0x00);
|
ret.bytecode.push_back(0x00);
|
||||||
bytesRef length(&ret.bytecode.back() + 1 - 2, 2);
|
toBigEndian(m_codeSections.size() * 2, bytesRef(&ret.bytecode.back() + 1 - 2, 2));
|
||||||
toBigEndian(m_codeSections.size() * 2, length);
|
|
||||||
}
|
}
|
||||||
for (auto const& codeSection: m_codeSections)
|
for (auto const& codeSection: m_codeSections)
|
||||||
{
|
{
|
||||||
@ -856,7 +857,6 @@ LinkerObject const& Assembly::assemble() const
|
|||||||
{
|
{
|
||||||
assertThrow(m_eofVersion.has_value(), AssemblyException, "Relative jump outside EOF");
|
assertThrow(m_eofVersion.has_value(), AssemblyException, "Relative jump outside EOF");
|
||||||
assertThrow(subId == numeric_limits<size_t>::max(), AssemblyException, "Relative jump to sub");
|
assertThrow(subId == numeric_limits<size_t>::max(), AssemblyException, "Relative jump to sub");
|
||||||
bytesRef r(ret.bytecode.data() + bytecodeOffset, 2);
|
|
||||||
assertThrow(
|
assertThrow(
|
||||||
static_cast<ssize_t>(pos) - static_cast<ssize_t>(bytecodeOffset + 2u) < 0x7FFF &&
|
static_cast<ssize_t>(pos) - static_cast<ssize_t>(bytecodeOffset + 2u) < 0x7FFF &&
|
||||||
static_cast<ssize_t>(pos) - static_cast<ssize_t>(bytecodeOffset + 2u) >= -0x8000,
|
static_cast<ssize_t>(pos) - static_cast<ssize_t>(bytecodeOffset + 2u) >= -0x8000,
|
||||||
@ -864,13 +864,12 @@ LinkerObject const& Assembly::assemble() const
|
|||||||
"Relative jump too far"
|
"Relative jump too far"
|
||||||
);
|
);
|
||||||
uint16_t relativeOffset = static_cast<uint16_t>(pos - (bytecodeOffset + 2u));
|
uint16_t relativeOffset = static_cast<uint16_t>(pos - (bytecodeOffset + 2u));
|
||||||
toBigEndian(relativeOffset, r);
|
toBigEndian(relativeOffset, bytesRef(ret.bytecode.data() + bytecodeOffset, 2));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
assertThrow(!m_eofVersion.has_value(), AssemblyException, "Dynamic tag reference within EOF");
|
assertThrow(!m_eofVersion.has_value(), AssemblyException, "Dynamic tag reference within EOF");
|
||||||
bytesRef r(ret.bytecode.data() + bytecodeOffset, bytesPerTag);
|
toBigEndian(pos, bytesRef(ret.bytecode.data() + bytecodeOffset, bytesPerTag));
|
||||||
toBigEndian(pos, r);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (auto const& [name, tagInfo]: m_namedTags)
|
for (auto const& [name, tagInfo]: m_namedTags)
|
||||||
@ -899,15 +898,13 @@ LinkerObject const& Assembly::assemble() const
|
|||||||
if (references.first == references.second)
|
if (references.first == references.second)
|
||||||
continue;
|
continue;
|
||||||
for (auto ref = references.first; ref != references.second; ++ref)
|
for (auto ref = references.first; ref != references.second; ++ref)
|
||||||
{
|
toBigEndian(ret.bytecode.size(), bytesRef(ret.bytecode.data() + ref->second, bytesPerDataRef));
|
||||||
bytesRef r(ret.bytecode.data() + ref->second, bytesPerDataRef);
|
|
||||||
toBigEndian(ret.bytecode.size(), r);
|
|
||||||
}
|
|
||||||
ret.bytecode += dataItem.second;
|
ret.bytecode += dataItem.second;
|
||||||
}
|
}
|
||||||
|
|
||||||
ret.bytecode += m_auxiliaryData;
|
ret.bytecode += m_auxiliaryData;
|
||||||
|
|
||||||
|
// TODO: remove this when transitioning to unified spec headers
|
||||||
if (eof && bytesRequiredForDataAndSubsUpperBound > 0 && ret.bytecode.size() == dataStart)
|
if (eof && bytesRequiredForDataAndSubsUpperBound > 0 && ret.bytecode.size() == dataStart)
|
||||||
{
|
{
|
||||||
// We have commited to a data section, but not actually needed it, so create a fake one.
|
// We have commited to a data section, but not actually needed it, so create a fake one.
|
||||||
|
@ -157,7 +157,6 @@ public:
|
|||||||
langutil::SourceLocation const& currentSourceLocation() const { return m_currentSourceLocation; }
|
langutil::SourceLocation const& currentSourceLocation() const { return m_currentSourceLocation; }
|
||||||
|
|
||||||
/// Assembles the assembly into bytecode. The assembly should not be modified after this call, since the assembled version is cached.
|
/// Assembles the assembly into bytecode. The assembly should not be modified after this call, since the assembled version is cached.
|
||||||
/// @param eof If true, assemble for EOF, otherwise for legacy EVM output.
|
|
||||||
LinkerObject const& assemble() const;
|
LinkerObject const& assemble() const;
|
||||||
|
|
||||||
struct OptimiserSettings
|
struct OptimiserSettings
|
||||||
|
@ -113,8 +113,8 @@ public:
|
|||||||
AssemblyItem& operator=(AssemblyItem const&) = default;
|
AssemblyItem& operator=(AssemblyItem const&) = default;
|
||||||
AssemblyItem& operator=(AssemblyItem&&) = default;
|
AssemblyItem& operator=(AssemblyItem&&) = default;
|
||||||
|
|
||||||
AssemblyItem tag() const { assertThrow(m_type == PushTag || m_type == Tag, util::Exception, ""); return AssemblyItem(Tag, data()); }
|
AssemblyItem tag() const { assertThrow(m_type == PushTag || m_type == Tag || m_type == RelativeJump || m_type == ConditionalRelativeJump, util::Exception, ""); return AssemblyItem(Tag, data()); }
|
||||||
AssemblyItem pushTag() const { assertThrow(m_type == PushTag || m_type == Tag, util::Exception, ""); return AssemblyItem(PushTag, data()); }
|
AssemblyItem pushTag() const { assertThrow(m_type == PushTag || m_type == Tag || m_type == RelativeJump || m_type == ConditionalRelativeJump, util::Exception, ""); return AssemblyItem(PushTag, data()); }
|
||||||
/// Converts the tag to a subassembly tag. This has to be called in order to move a tag across assemblies.
|
/// Converts the tag to a subassembly tag. This has to be called in order to move a tag across assemblies.
|
||||||
/// @param _subId the identifier of the subassembly the tag is taken from.
|
/// @param _subId the identifier of the subassembly the tag is taken from.
|
||||||
AssemblyItem toSubAssemblyTag(size_t _subId) const;
|
AssemblyItem toSubAssemblyTag(size_t _subId) const;
|
||||||
|
@ -132,7 +132,7 @@ BlockDeduplicator::BlockIterator& BlockDeduplicator::BlockIterator::operator++()
|
|||||||
{
|
{
|
||||||
if (it == end)
|
if (it == end)
|
||||||
return *this;
|
return *this;
|
||||||
if (SemanticInformation::altersControlFlow(*it) && *it != AssemblyItem{Instruction::JUMPI})
|
if (SemanticInformation::altersControlFlow(*it) && *it != AssemblyItem{Instruction::JUMPI} && it->type() != ConditionalRelativeJump)
|
||||||
it = end;
|
it = end;
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -165,6 +165,9 @@ std::map<std::string, Instruction> const solidity::evmasm::c_instructions =
|
|||||||
{ "LOG2", Instruction::LOG2 },
|
{ "LOG2", Instruction::LOG2 },
|
||||||
{ "LOG3", Instruction::LOG3 },
|
{ "LOG3", Instruction::LOG3 },
|
||||||
{ "LOG4", Instruction::LOG4 },
|
{ "LOG4", Instruction::LOG4 },
|
||||||
|
{ "CALLF", Instruction::CALLF },
|
||||||
|
{ "RETF", Instruction::RETF },
|
||||||
|
{ "JUMPF", Instruction::JUMPF },
|
||||||
{ "CREATE", Instruction::CREATE },
|
{ "CREATE", Instruction::CREATE },
|
||||||
{ "CALL", Instruction::CALL },
|
{ "CALL", Instruction::CALL },
|
||||||
{ "CALLCODE", Instruction::CALLCODE },
|
{ "CALLCODE", Instruction::CALLCODE },
|
||||||
@ -243,9 +246,9 @@ static std::map<Instruction, InstructionInfo> const c_instructionInfo =
|
|||||||
{ Instruction::MSIZE, { "MSIZE", 0, 0, 1, false, Tier::Base } },
|
{ Instruction::MSIZE, { "MSIZE", 0, 0, 1, false, Tier::Base } },
|
||||||
{ Instruction::GAS, { "GAS", 0, 0, 1, false, Tier::Base } },
|
{ Instruction::GAS, { "GAS", 0, 0, 1, false, Tier::Base } },
|
||||||
{ Instruction::JUMPDEST, { "JUMPDEST", 0, 0, 0, true, Tier::Special } },
|
{ Instruction::JUMPDEST, { "JUMPDEST", 0, 0, 0, true, Tier::Special } },
|
||||||
{ Instruction::RJUMP, { "RJUMP", 0, 0, 0, true, Tier::Low } },
|
{ Instruction::RJUMP, { "RJUMP", 2, 0, 0, true, Tier::Low } },
|
||||||
{ Instruction::RJUMPI, { "RJUMPI", 0, 0, 0, true, Tier::Low } },
|
{ Instruction::RJUMPI, { "RJUMPI", 2, 1, 0, true, Tier::Low } },
|
||||||
{ Instruction::RJUMPV, { "RJUMPV", 0, 1, 0, true, Tier::Low } },
|
{ Instruction::RJUMPV, { "RJUMPV", 2, 1, 0, true, Tier::Low } },
|
||||||
{ Instruction::PUSH1, { "PUSH1", 1, 0, 1, false, Tier::VeryLow } },
|
{ Instruction::PUSH1, { "PUSH1", 1, 0, 1, false, Tier::VeryLow } },
|
||||||
{ Instruction::PUSH2, { "PUSH2", 2, 0, 1, false, Tier::VeryLow } },
|
{ Instruction::PUSH2, { "PUSH2", 2, 0, 1, false, Tier::VeryLow } },
|
||||||
{ Instruction::PUSH3, { "PUSH3", 3, 0, 1, false, Tier::VeryLow } },
|
{ Instruction::PUSH3, { "PUSH3", 3, 0, 1, false, Tier::VeryLow } },
|
||||||
@ -315,6 +318,9 @@ static std::map<Instruction, InstructionInfo> const c_instructionInfo =
|
|||||||
{ Instruction::LOG2, { "LOG2", 0, 4, 0, true, Tier::Special } },
|
{ Instruction::LOG2, { "LOG2", 0, 4, 0, true, Tier::Special } },
|
||||||
{ Instruction::LOG3, { "LOG3", 0, 5, 0, true, Tier::Special } },
|
{ Instruction::LOG3, { "LOG3", 0, 5, 0, true, Tier::Special } },
|
||||||
{ Instruction::LOG4, { "LOG4", 0, 6, 0, true, Tier::Special } },
|
{ Instruction::LOG4, { "LOG4", 0, 6, 0, true, Tier::Special } },
|
||||||
|
{ Instruction::CALLF, { "CALLF", 2, 0, 0, true, Tier::Special } },
|
||||||
|
{ Instruction::RETF, { "RETF", 0, 0, 0, true, Tier::Special } },
|
||||||
|
{ Instruction::JUMPF, { "JUMPF", 2, 0, 0, true, Tier::Special } },
|
||||||
{ Instruction::CREATE, { "CREATE", 0, 3, 1, true, Tier::Special } },
|
{ Instruction::CREATE, { "CREATE", 0, 3, 1, true, Tier::Special } },
|
||||||
{ Instruction::CALL, { "CALL", 0, 7, 1, true, Tier::Special } },
|
{ Instruction::CALL, { "CALL", 0, 7, 1, true, Tier::Special } },
|
||||||
{ Instruction::CALLCODE, { "CALLCODE", 0, 7, 1, true, Tier::Special } },
|
{ Instruction::CALLCODE, { "CALLCODE", 0, 7, 1, true, Tier::Special } },
|
||||||
|
@ -291,7 +291,7 @@ struct IsZeroIsZeroJumpI: SimplePeepholeOptimizerMethod<IsZeroIsZeroJumpI>
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
struct IsZeroIsZeroRJumpI: SimplePeepholeOptimizerMethod<IsZeroIsZeroJumpI>
|
struct IsZeroIsZeroRJumpI: SimplePeepholeOptimizerMethod<IsZeroIsZeroRJumpI>
|
||||||
{
|
{
|
||||||
static size_t applySimple(
|
static size_t applySimple(
|
||||||
AssemblyItem const& _iszero1,
|
AssemblyItem const& _iszero1,
|
||||||
@ -341,7 +341,7 @@ struct EqIsZeroJumpI: SimplePeepholeOptimizerMethod<EqIsZeroJumpI>
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
struct EqIsZeroRJumpI: SimplePeepholeOptimizerMethod<EqIsZeroJumpI>
|
struct EqIsZeroRJumpI: SimplePeepholeOptimizerMethod<EqIsZeroRJumpI>
|
||||||
{
|
{
|
||||||
static size_t applySimple(
|
static size_t applySimple(
|
||||||
AssemblyItem const& _eq,
|
AssemblyItem const& _eq,
|
||||||
@ -398,7 +398,7 @@ struct DoubleJump: SimplePeepholeOptimizerMethod<DoubleJump>
|
|||||||
};
|
};
|
||||||
|
|
||||||
// rjumpi(tag_1) rjump(tag_2) tag_1: -> iszero rjumpi(tag_2) tag_1:
|
// rjumpi(tag_1) rjump(tag_2) tag_1: -> iszero rjumpi(tag_2) tag_1:
|
||||||
struct DoubleRJump: SimplePeepholeOptimizerMethod<DoubleJump>
|
struct DoubleRJump: SimplePeepholeOptimizerMethod<DoubleRJump>
|
||||||
{
|
{
|
||||||
static size_t applySimple(
|
static size_t applySimple(
|
||||||
AssemblyItem const& _rjumpi,
|
AssemblyItem const& _rjumpi,
|
||||||
@ -450,7 +450,7 @@ struct JumpToNext: SimplePeepholeOptimizerMethod<JumpToNext>
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
struct RJumpToNext: SimplePeepholeOptimizerMethod<JumpToNext>
|
struct RJumpToNext: SimplePeepholeOptimizerMethod<RJumpToNext>
|
||||||
{
|
{
|
||||||
static size_t applySimple(
|
static size_t applySimple(
|
||||||
AssemblyItem const& _rjump,
|
AssemblyItem const& _rjump,
|
||||||
|
@ -239,7 +239,7 @@ bool SemanticInformation::isJumpInstruction(AssemblyItem const& _item)
|
|||||||
|
|
||||||
bool SemanticInformation::altersControlFlow(AssemblyItem const& _item)
|
bool SemanticInformation::altersControlFlow(AssemblyItem const& _item)
|
||||||
{
|
{
|
||||||
if (_item.type() == evmasm::RetF)
|
if (_item.type() == evmasm::RetF || _item.type() == evmasm::RelativeJump || _item.type() == evmasm::ConditionalRelativeJump)
|
||||||
return true;
|
return true;
|
||||||
if (_item.type() != evmasm::Operation)
|
if (_item.type() != evmasm::Operation)
|
||||||
return false;
|
return false;
|
||||||
@ -249,6 +249,9 @@ bool SemanticInformation::altersControlFlow(AssemblyItem const& _item)
|
|||||||
// continue on the next instruction
|
// continue on the next instruction
|
||||||
case Instruction::JUMP:
|
case Instruction::JUMP:
|
||||||
case Instruction::JUMPI:
|
case Instruction::JUMPI:
|
||||||
|
case Instruction::RJUMP:
|
||||||
|
case Instruction::RJUMPI:
|
||||||
|
case Instruction::RJUMPV:
|
||||||
case Instruction::RETURN:
|
case Instruction::RETURN:
|
||||||
case Instruction::SELFDESTRUCT:
|
case Instruction::SELFDESTRUCT:
|
||||||
case Instruction::STOP:
|
case Instruction::STOP:
|
||||||
|
@ -99,13 +99,13 @@ bool fitsPrecisionBaseX(bigint const& _mantissa, double _log2OfBase, uint32_t _e
|
|||||||
/// @a Out will typically be either std::string or bytes.
|
/// @a Out will typically be either std::string or bytes.
|
||||||
/// @a T will typically by unsigned, u160, u256 or bigint.
|
/// @a T will typically by unsigned, u160, u256 or bigint.
|
||||||
template <class T, class Out>
|
template <class T, class Out>
|
||||||
inline void toBigEndian(T _val, Out& o_out)
|
inline void toBigEndian(T _val, Out&& o_out)
|
||||||
{
|
{
|
||||||
static_assert(std::is_same<bigint, T>::value || !std::numeric_limits<T>::is_signed, "only unsigned types or bigint supported"); //bigint does not carry sign bit on shift
|
static_assert(std::is_same<bigint, T>::value || !std::numeric_limits<T>::is_signed, "only unsigned types or bigint supported"); //bigint does not carry sign bit on shift
|
||||||
for (auto i = o_out.size(); i != 0; _val >>= 8, i--)
|
for (auto i = o_out.size(); i != 0; _val >>= 8, i--)
|
||||||
{
|
{
|
||||||
T v = _val & (T)0xff;
|
T v = _val & (T)0xff;
|
||||||
o_out[i - 1] = (typename Out::value_type)(uint8_t)v;
|
o_out[i - 1] = (typename std::remove_reference_t<Out>::value_type)(uint8_t)v;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,7 +60,6 @@ public:
|
|||||||
|
|
||||||
virtual ~AbstractAssembly() = default;
|
virtual ~AbstractAssembly() = default;
|
||||||
|
|
||||||
virtual bool supportsFunctions() const = 0;
|
|
||||||
/// Set a new source location valid starting from the next instruction.
|
/// Set a new source location valid starting from the next instruction.
|
||||||
virtual void setSourceLocation(langutil::SourceLocation const& _location) = 0;
|
virtual void setSourceLocation(langutil::SourceLocation const& _location) = 0;
|
||||||
/// Retrieve the current height of the stack. This does not have to be zero
|
/// Retrieve the current height of the stack. This does not have to be zero
|
||||||
|
@ -44,11 +44,6 @@ EthAssemblyAdapter::EthAssemblyAdapter(evmasm::Assembly& _assembly):
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
bool EthAssemblyAdapter::supportsFunctions() const
|
|
||||||
{
|
|
||||||
return m_assembly.supportsFunctions();
|
|
||||||
}
|
|
||||||
|
|
||||||
void EthAssemblyAdapter::setSourceLocation(SourceLocation const& _location)
|
void EthAssemblyAdapter::setSourceLocation(SourceLocation const& _location)
|
||||||
{
|
{
|
||||||
m_assembly.setSourceLocation(_location);
|
m_assembly.setSourceLocation(_location);
|
||||||
|
@ -40,7 +40,6 @@ class EthAssemblyAdapter: public AbstractAssembly
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
explicit EthAssemblyAdapter(evmasm::Assembly& _assembly);
|
explicit EthAssemblyAdapter(evmasm::Assembly& _assembly);
|
||||||
bool supportsFunctions() const override;
|
|
||||||
void setSourceLocation(langutil::SourceLocation const& _location) override;
|
void setSourceLocation(langutil::SourceLocation const& _location) override;
|
||||||
int stackHeight() const override;
|
int stackHeight() const override;
|
||||||
void setStackHeight(int height) override;
|
void setStackHeight(int height) override;
|
||||||
|
@ -57,8 +57,6 @@ public:
|
|||||||
|
|
||||||
~NoOutputAssembly() override = default;
|
~NoOutputAssembly() override = default;
|
||||||
|
|
||||||
bool supportsFunctions() const override { return m_hasFunctions; }
|
|
||||||
|
|
||||||
void setSourceLocation(langutil::SourceLocation const&) override {}
|
void setSourceLocation(langutil::SourceLocation const&) override {}
|
||||||
int stackHeight() const override { return m_stackHeight; }
|
int stackHeight() const override { return m_stackHeight; }
|
||||||
void setStackHeight(int height) override { m_stackHeight = height; }
|
void setStackHeight(int height) override { m_stackHeight = height; }
|
||||||
|
Loading…
Reference in New Issue
Block a user