mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-02-21 14:34:49 +01:00
Merge bitcoin/bitcoin#29464: [25.2] Final backports and changes for 25.2rc1
9f13dc1ed3
doc: Update release notes for 25.2rc1 (Ava Chow)a27662b16a
doc: update manpages for 25.2rc1 (Ava Chow)65c6171784
build: Bump to 25.2rc1 (Ava Chow)cf0f43ee42
wallet: Fix use-after-free in WalletBatch::EraseRecords (MarcoFalke)6acfc4324c
Use only Span{} constructor for byte-like types where possible (MarcoFalke)b40d10787b
util: Allow std::byte and char Span serialization (MarcoFalke) Pull request description: Backport: * #29176 * #27927 #29176 does not cleanly backport, and it also requires 27927 to work. Both are still fairly simple backports. Also does the rest of the version bump tasks for 25.2rc1. ACKs for top commit: fanquake: ACK9f13dc1ed3
Tree-SHA512: 9d9dbf415f8559410eba9a431b61a8fc94216898d2d1fd8398e1f7a22a04790faade810e65324c7a797456b33396c3a58f991e81319aaaa63d3ab441e5e20dbc
This commit is contained in:
commit
1ce5accc32
18 changed files with 66 additions and 53 deletions
|
@ -1,8 +1,8 @@
|
|||
AC_PREREQ([2.69])
|
||||
define(_CLIENT_VERSION_MAJOR, 25)
|
||||
define(_CLIENT_VERSION_MINOR, 1)
|
||||
define(_CLIENT_VERSION_MINOR, 2)
|
||||
define(_CLIENT_VERSION_BUILD, 0)
|
||||
define(_CLIENT_VERSION_RC, 0)
|
||||
define(_CLIENT_VERSION_RC, 1)
|
||||
define(_CLIENT_VERSION_IS_RELEASE, true)
|
||||
define(_COPYRIGHT_YEAR, 2023)
|
||||
define(_COPYRIGHT_HOLDERS,[The %s developers])
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3.
|
||||
.TH BITCOIN-CLI "1" "October 2023" "bitcoin-cli v25.1.0" "User Commands"
|
||||
.TH BITCOIN-CLI "1" "February 2024" "bitcoin-cli v25.2.0rc1" "User Commands"
|
||||
.SH NAME
|
||||
bitcoin-cli \- manual page for bitcoin-cli v25.1.0
|
||||
bitcoin-cli \- manual page for bitcoin-cli v25.2.0rc1
|
||||
.SH SYNOPSIS
|
||||
.B bitcoin-cli
|
||||
[\fI\,options\/\fR] \fI\,<command> \/\fR[\fI\,params\/\fR] \fI\,Send command to Bitcoin Core\/\fR
|
||||
|
@ -15,7 +15,7 @@ bitcoin-cli \- manual page for bitcoin-cli v25.1.0
|
|||
.B bitcoin-cli
|
||||
[\fI\,options\/\fR] \fI\,help <command> Get help for a command\/\fR
|
||||
.SH DESCRIPTION
|
||||
Bitcoin Core RPC client version v25.1.0
|
||||
Bitcoin Core RPC client version v25.2.0rc1
|
||||
.SH OPTIONS
|
||||
.HP
|
||||
\-?
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3.
|
||||
.TH BITCOIN-QT "1" "October 2023" "bitcoin-qt v25.1.0" "User Commands"
|
||||
.TH BITCOIN-QT "1" "February 2024" "bitcoin-qt v25.2.0rc1" "User Commands"
|
||||
.SH NAME
|
||||
bitcoin-qt \- manual page for bitcoin-qt v25.1.0
|
||||
bitcoin-qt \- manual page for bitcoin-qt v25.2.0rc1
|
||||
.SH SYNOPSIS
|
||||
.B bitcoin-qt
|
||||
[\fI\,command-line options\/\fR]
|
||||
.SH DESCRIPTION
|
||||
Bitcoin Core version v25.1.0
|
||||
Bitcoin Core version v25.2.0rc1
|
||||
.SH OPTIONS
|
||||
.HP
|
||||
\-?
|
||||
|
@ -116,7 +116,7 @@ Do not keep transactions in the mempool longer than <n> hours (default:
|
|||
.HP
|
||||
\fB\-par=\fR<n>
|
||||
.IP
|
||||
Set the number of script verification threads (\fB\-10\fR to 15, 0 = auto, <0 =
|
||||
Set the number of script verification threads (\fB\-64\fR to 15, 0 = auto, <0 =
|
||||
leave that many cores free, default: 0)
|
||||
.HP
|
||||
\fB\-persistmempool\fR
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3.
|
||||
.TH BITCOIN-TX "1" "October 2023" "bitcoin-tx v25.1.0" "User Commands"
|
||||
.TH BITCOIN-TX "1" "February 2024" "bitcoin-tx v25.2.0rc1" "User Commands"
|
||||
.SH NAME
|
||||
bitcoin-tx \- manual page for bitcoin-tx v25.1.0
|
||||
bitcoin-tx \- manual page for bitcoin-tx v25.2.0rc1
|
||||
.SH SYNOPSIS
|
||||
.B bitcoin-tx
|
||||
[\fI\,options\/\fR] \fI\,<hex-tx> \/\fR[\fI\,commands\/\fR] \fI\,Update hex-encoded bitcoin transaction\/\fR
|
||||
|
@ -9,7 +9,7 @@ bitcoin-tx \- manual page for bitcoin-tx v25.1.0
|
|||
.B bitcoin-tx
|
||||
[\fI\,options\/\fR] \fI\,-create \/\fR[\fI\,commands\/\fR] \fI\,Create hex-encoded bitcoin transaction\/\fR
|
||||
.SH DESCRIPTION
|
||||
Bitcoin Core bitcoin\-tx utility version v25.1.0
|
||||
Bitcoin Core bitcoin\-tx utility version v25.2.0rc1
|
||||
.SH OPTIONS
|
||||
.HP
|
||||
\-?
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3.
|
||||
.TH BITCOIN-UTIL "1" "October 2023" "bitcoin-util v25.1.0" "User Commands"
|
||||
.TH BITCOIN-UTIL "1" "February 2024" "bitcoin-util v25.2.0rc1" "User Commands"
|
||||
.SH NAME
|
||||
bitcoin-util \- manual page for bitcoin-util v25.1.0
|
||||
bitcoin-util \- manual page for bitcoin-util v25.2.0rc1
|
||||
.SH SYNOPSIS
|
||||
.B bitcoin-util
|
||||
[\fI\,options\/\fR] [\fI\,commands\/\fR] \fI\,Do stuff\/\fR
|
||||
.SH DESCRIPTION
|
||||
Bitcoin Core bitcoin\-util utility version v25.1.0
|
||||
Bitcoin Core bitcoin\-util utility version v25.2.0rc1
|
||||
.SH OPTIONS
|
||||
.HP
|
||||
\-?
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3.
|
||||
.TH BITCOIN-WALLET "1" "October 2023" "bitcoin-wallet v25.1.0" "User Commands"
|
||||
.TH BITCOIN-WALLET "1" "February 2024" "bitcoin-wallet v25.2.0rc1" "User Commands"
|
||||
.SH NAME
|
||||
bitcoin-wallet \- manual page for bitcoin-wallet v25.1.0
|
||||
bitcoin-wallet \- manual page for bitcoin-wallet v25.2.0rc1
|
||||
.SH DESCRIPTION
|
||||
Bitcoin Core bitcoin\-wallet version v25.1.0
|
||||
Bitcoin Core bitcoin\-wallet version v25.2.0rc1
|
||||
.PP
|
||||
bitcoin\-wallet is an offline tool for creating and interacting with Bitcoin Core wallet files.
|
||||
By default bitcoin\-wallet will act on wallets in the default mainnet wallet directory in the datadir.
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3.
|
||||
.TH BITCOIND "1" "October 2023" "bitcoind v25.1.0" "User Commands"
|
||||
.TH BITCOIND "1" "February 2024" "bitcoind v25.2.0rc1" "User Commands"
|
||||
.SH NAME
|
||||
bitcoind \- manual page for bitcoind v25.1.0
|
||||
bitcoind \- manual page for bitcoind v25.2.0rc1
|
||||
.SH SYNOPSIS
|
||||
.B bitcoind
|
||||
[\fI\,options\/\fR] \fI\,Start Bitcoin Core\/\fR
|
||||
.SH DESCRIPTION
|
||||
Bitcoin Core version v25.1.0
|
||||
Bitcoin Core version v25.2.0rc1
|
||||
.SH OPTIONS
|
||||
.HP
|
||||
\-?
|
||||
|
@ -116,7 +116,7 @@ Do not keep transactions in the mempool longer than <n> hours (default:
|
|||
.HP
|
||||
\fB\-par=\fR<n>
|
||||
.IP
|
||||
Set the number of script verification threads (\fB\-10\fR to 15, 0 = auto, <0 =
|
||||
Set the number of script verification threads (\fB\-64\fR to 15, 0 = auto, <0 =
|
||||
leave that many cores free, default: 0)
|
||||
.HP
|
||||
\fB\-persistmempool\fR
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
25.x Release Notes
|
||||
25.2rc1 Release Notes
|
||||
==================
|
||||
|
||||
Bitcoin Core version 25.x is now available from:
|
||||
Bitcoin Core version 25.2rc1 is now available from:
|
||||
|
||||
<https://bitcoincore.org/bin/bitcoin-core-25.x/>
|
||||
<https://bitcoincore.org/bin/bitcoin-core-25.2/test.rc1>
|
||||
|
||||
This release includes various bug fixes and performance
|
||||
improvements, as well as updated translations.
|
||||
|
@ -48,6 +48,10 @@ Notable changes
|
|||
|
||||
- #29003 rpc: fix getrawtransaction segfault
|
||||
|
||||
### Wallet
|
||||
|
||||
- #29176 wallet: Fix use-after-free in WalletBatch::EraseRecords
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
|
@ -55,6 +59,7 @@ Thanks to everyone who directly contributed to this release:
|
|||
|
||||
- Martin Zumsande
|
||||
- Sebastian Falbesoner
|
||||
- MarcoFalke
|
||||
|
||||
As well as to everyone that helped with translations on
|
||||
[Transifex](https://www.transifex.com/bitcoin/bitcoin/).
|
||||
|
|
|
@ -33,7 +33,7 @@ static void LoadExternalBlockFile(benchmark::Bench& bench)
|
|||
ss << static_cast<uint32_t>(benchmark::data::block413567.size());
|
||||
// We can't use the streaming serialization (ss << benchmark::data::block413567)
|
||||
// because that first writes a compact size.
|
||||
ss.write(MakeByteSpan(benchmark::data::block413567));
|
||||
ss << Span{benchmark::data::block413567};
|
||||
|
||||
// Create the test file.
|
||||
{
|
||||
|
|
|
@ -2939,13 +2939,13 @@ void CaptureMessageToFile(const CAddress& addr,
|
|||
AutoFile f{fsbridge::fopen(path, "ab")};
|
||||
|
||||
ser_writedata64(f, now.count());
|
||||
f.write(MakeByteSpan(msg_type));
|
||||
f << Span{msg_type};
|
||||
for (auto i = msg_type.length(); i < CMessageHeader::COMMAND_SIZE; ++i) {
|
||||
f << uint8_t{'\0'};
|
||||
}
|
||||
uint32_t size = data.size();
|
||||
ser_writedata32(f, size);
|
||||
f.write(AsBytes(data));
|
||||
f << data;
|
||||
}
|
||||
|
||||
std::function<void(const CAddress& addr,
|
||||
|
|
|
@ -142,14 +142,14 @@ public:
|
|||
{
|
||||
unsigned int len = size();
|
||||
::WriteCompactSize(s, len);
|
||||
s.write(AsBytes(Span{vch, len}));
|
||||
s << Span{vch, len};
|
||||
}
|
||||
template <typename Stream>
|
||||
void Unserialize(Stream& s)
|
||||
{
|
||||
const unsigned int len(::ReadCompactSize(s));
|
||||
if (len <= SIZE) {
|
||||
s.read(AsWritableBytes(Span{vch, len}));
|
||||
s >> Span{vch, len};
|
||||
if (len != size()) {
|
||||
Invalidate();
|
||||
}
|
||||
|
|
|
@ -188,6 +188,7 @@ template<typename X> const X& ReadWriteAsHelper(const X& x) { return x; }
|
|||
} \
|
||||
FORMATTER_METHODS(cls, obj)
|
||||
|
||||
// clang-format off
|
||||
#ifndef CHAR_EQUALS_INT8
|
||||
template <typename Stream> void Serialize(Stream&, char) = delete; // char serialization forbidden. Use uint8_t or int8_t
|
||||
#endif
|
||||
|
@ -201,8 +202,7 @@ template<typename Stream> inline void Serialize(Stream& s, int64_t a ) { ser_wri
|
|||
template<typename Stream> inline void Serialize(Stream& s, uint64_t a) { ser_writedata64(s, a); }
|
||||
template<typename Stream, int N> inline void Serialize(Stream& s, const char (&a)[N]) { s.write(MakeByteSpan(a)); }
|
||||
template<typename Stream, int N> inline void Serialize(Stream& s, const unsigned char (&a)[N]) { s.write(MakeByteSpan(a)); }
|
||||
template<typename Stream> inline void Serialize(Stream& s, const Span<const unsigned char>& span) { s.write(AsBytes(span)); }
|
||||
template<typename Stream> inline void Serialize(Stream& s, const Span<unsigned char>& span) { s.write(AsBytes(span)); }
|
||||
template <typename Stream, typename B> void Serialize(Stream& s, Span<B> span) { (void)/* force byte-type */UCharCast(span.data()); s.write(AsBytes(span)); }
|
||||
|
||||
#ifndef CHAR_EQUALS_INT8
|
||||
template <typename Stream> void Unserialize(Stream&, char) = delete; // char serialization forbidden. Use uint8_t or int8_t
|
||||
|
@ -217,10 +217,11 @@ template<typename Stream> inline void Unserialize(Stream& s, int64_t& a ) { a =
|
|||
template<typename Stream> inline void Unserialize(Stream& s, uint64_t& a) { a = ser_readdata64(s); }
|
||||
template<typename Stream, int N> inline void Unserialize(Stream& s, char (&a)[N]) { s.read(MakeWritableByteSpan(a)); }
|
||||
template<typename Stream, int N> inline void Unserialize(Stream& s, unsigned char (&a)[N]) { s.read(MakeWritableByteSpan(a)); }
|
||||
template<typename Stream> inline void Unserialize(Stream& s, Span<unsigned char>& span) { s.read(AsWritableBytes(span)); }
|
||||
template <typename Stream, typename B> void Unserialize(Stream& s, Span<B> span) { (void)/* force byte-type */UCharCast(span.data()); s.read(AsWritableBytes(span)); }
|
||||
|
||||
template <typename Stream> inline void Serialize(Stream& s, bool a) { uint8_t f = a; ser_writedata8(s, f); }
|
||||
template <typename Stream> inline void Unserialize(Stream& s, bool& a) { uint8_t f = ser_readdata8(s); a = f; }
|
||||
// clang-format on
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
@ -76,7 +76,7 @@ FUZZ_TARGET_INIT(p2p_transport_serialization, initialize_p2p_transport_serializa
|
|||
assert(msg.m_time == m_time);
|
||||
|
||||
std::vector<unsigned char> header;
|
||||
auto msg2 = CNetMsgMaker{msg.m_recv.GetVersion()}.Make(msg.m_type, MakeUCharSpan(msg.m_recv));
|
||||
auto msg2 = CNetMsgMaker{msg.m_recv.GetVersion()}.Make(msg.m_type, Span{msg.m_recv});
|
||||
serializer.prepareForTransport(msg2, header);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -186,32 +186,32 @@ BOOST_AUTO_TEST_CASE(noncanonical)
|
|||
std::vector<char>::size_type n;
|
||||
|
||||
// zero encoded with three bytes:
|
||||
ss.write(MakeByteSpan("\xfd\x00\x00").first(3));
|
||||
ss << Span{"\xfd\x00\x00"}.first(3);
|
||||
BOOST_CHECK_EXCEPTION(ReadCompactSize(ss), std::ios_base::failure, isCanonicalException);
|
||||
|
||||
// 0xfc encoded with three bytes:
|
||||
ss.write(MakeByteSpan("\xfd\xfc\x00").first(3));
|
||||
ss << Span{"\xfd\xfc\x00"}.first(3);
|
||||
BOOST_CHECK_EXCEPTION(ReadCompactSize(ss), std::ios_base::failure, isCanonicalException);
|
||||
|
||||
// 0xfd encoded with three bytes is OK:
|
||||
ss.write(MakeByteSpan("\xfd\xfd\x00").first(3));
|
||||
ss << Span{"\xfd\xfd\x00"}.first(3);
|
||||
n = ReadCompactSize(ss);
|
||||
BOOST_CHECK(n == 0xfd);
|
||||
|
||||
// zero encoded with five bytes:
|
||||
ss.write(MakeByteSpan("\xfe\x00\x00\x00\x00").first(5));
|
||||
ss << Span{"\xfe\x00\x00\x00\x00"}.first(5);
|
||||
BOOST_CHECK_EXCEPTION(ReadCompactSize(ss), std::ios_base::failure, isCanonicalException);
|
||||
|
||||
// 0xffff encoded with five bytes:
|
||||
ss.write(MakeByteSpan("\xfe\xff\xff\x00\x00").first(5));
|
||||
ss << Span{"\xfe\xff\xff\x00\x00"}.first(5);
|
||||
BOOST_CHECK_EXCEPTION(ReadCompactSize(ss), std::ios_base::failure, isCanonicalException);
|
||||
|
||||
// zero encoded with nine bytes:
|
||||
ss.write(MakeByteSpan("\xff\x00\x00\x00\x00\x00\x00\x00\x00").first(9));
|
||||
ss << Span{"\xff\x00\x00\x00\x00\x00\x00\x00\x00"}.first(9);
|
||||
BOOST_CHECK_EXCEPTION(ReadCompactSize(ss), std::ios_base::failure, isCanonicalException);
|
||||
|
||||
// 0x01ffffff encoded with nine bytes:
|
||||
ss.write(MakeByteSpan("\xff\xff\xff\xff\x01\x00\x00\x00\x00").first(9));
|
||||
ss << Span{"\xff\xff\xff\xff\x01\x00\x00\x00\x00"}.first(9);
|
||||
BOOST_CHECK_EXCEPTION(ReadCompactSize(ss), std::ios_base::failure, isCanonicalException);
|
||||
}
|
||||
|
||||
|
@ -241,6 +241,15 @@ BOOST_AUTO_TEST_CASE(class_methods)
|
|||
ss2 << intval << boolval << stringval << charstrval << txval;
|
||||
ss2 >> methodtest3;
|
||||
BOOST_CHECK(methodtest3 == methodtest4);
|
||||
{
|
||||
DataStream ds;
|
||||
const std::string in{"ab"};
|
||||
ds << Span{in};
|
||||
std::array<std::byte, 2> out;
|
||||
ds >> Span{out};
|
||||
BOOST_CHECK_EQUAL(out.at(0), std::byte{'a'});
|
||||
BOOST_CHECK_EQUAL(out.at(1), std::byte{'b'});
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
|
|
|
@ -77,7 +77,7 @@ public:
|
|||
template<typename Stream>
|
||||
void Serialize(Stream& s) const
|
||||
{
|
||||
s.write(MakeByteSpan(m_data));
|
||||
s << Span(m_data);
|
||||
}
|
||||
|
||||
template<typename Stream>
|
||||
|
|
|
@ -57,12 +57,12 @@ bool DumpWallet(const ArgsManager& args, CWallet& wallet, bilingual_str& error)
|
|||
// Write out a magic string with version
|
||||
std::string line = strprintf("%s,%u\n", DUMP_MAGIC, DUMP_VERSION);
|
||||
dump_file.write(line.data(), line.size());
|
||||
hasher.write(MakeByteSpan(line));
|
||||
hasher << Span{line};
|
||||
|
||||
// Write out the file format
|
||||
line = strprintf("%s,%s\n", "format", db.Format());
|
||||
dump_file.write(line.data(), line.size());
|
||||
hasher.write(MakeByteSpan(line));
|
||||
hasher << Span{line};
|
||||
|
||||
if (ret) {
|
||||
|
||||
|
@ -83,7 +83,7 @@ bool DumpWallet(const ArgsManager& args, CWallet& wallet, bilingual_str& error)
|
|||
std::string value_str = HexStr(ss_value);
|
||||
line = strprintf("%s,%s\n", key_str, value_str);
|
||||
dump_file.write(line.data(), line.size());
|
||||
hasher.write(MakeByteSpan(line));
|
||||
hasher << Span{line};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -160,7 +160,7 @@ bool CreateFromDump(const ArgsManager& args, const std::string& name, const fs::
|
|||
return false;
|
||||
}
|
||||
std::string magic_hasher_line = strprintf("%s,%s\n", magic_key, version_value);
|
||||
hasher.write(MakeByteSpan(magic_hasher_line));
|
||||
hasher << Span{magic_hasher_line};
|
||||
|
||||
// Get the stored file format
|
||||
std::string format_key;
|
||||
|
@ -191,7 +191,7 @@ bool CreateFromDump(const ArgsManager& args, const std::string& name, const fs::
|
|||
warnings.push_back(strprintf(_("Warning: Dumpfile wallet format \"%s\" does not match command line specified format \"%s\"."), format_value, file_format));
|
||||
}
|
||||
std::string format_hasher_line = strprintf("%s,%s\n", format_key, format_value);
|
||||
hasher.write(MakeByteSpan(format_hasher_line));
|
||||
hasher << Span{format_hasher_line};
|
||||
|
||||
DatabaseOptions options;
|
||||
DatabaseStatus status;
|
||||
|
@ -236,7 +236,7 @@ bool CreateFromDump(const ArgsManager& args, const std::string& name, const fs::
|
|||
}
|
||||
|
||||
std::string line = strprintf("%s,%s\n", key, value);
|
||||
hasher.write(MakeByteSpan(line));
|
||||
hasher << Span{line};
|
||||
|
||||
if (key.empty() || value.empty()) {
|
||||
continue;
|
||||
|
|
|
@ -3863,9 +3863,7 @@ bool CWallet::MigrateToSQLite(bilingual_str& error)
|
|||
bool began = batch->TxnBegin();
|
||||
assert(began); // This is a critical error, the new db could not be written to. The original db exists as a backup, but we should not continue execution.
|
||||
for (const auto& [key, value] : records) {
|
||||
DataStream ss_key{key};
|
||||
DataStream ss_value{value};
|
||||
if (!batch->Write(ss_key, ss_value)) {
|
||||
if (!batch->Write(Span{key}, Span{value})) {
|
||||
batch->TxnAbort();
|
||||
m_database->Close();
|
||||
fs::remove(m_database->Filename());
|
||||
|
|
|
@ -1132,13 +1132,13 @@ bool WalletBatch::EraseRecords(const std::unordered_set<std::string>& types)
|
|||
}
|
||||
|
||||
// Make a copy of key to avoid data being deleted by the following read of the type
|
||||
Span<const unsigned char> key_data = MakeUCharSpan(key);
|
||||
const SerializeData key_data{key.begin(), key.end()};
|
||||
|
||||
std::string type;
|
||||
key >> type;
|
||||
|
||||
if (types.count(type) > 0) {
|
||||
m_batch->Erase(key_data);
|
||||
m_batch->Erase(Span{key_data});
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
|
Loading…
Add table
Reference in a new issue