Commit 422b8ef0 authored by Johan Pascal's avatar Johan Pascal

Share RNG context between all DR Sessions linked to a Lime object

parent 530c404c
......@@ -161,9 +161,9 @@ namespace lime {
* @param[in] X3DH_initMessage at session creation as sender we shall also store the X3DHInit message to be able to include it in all message until we got a response from peer
*/
template <typename Curve>
DR<Curve>::DR(lime::Db *localStorage, const DRChainKey &SK, const SharedADBuffer &AD, const X<Curve> &peerPublicKey, long int peerDid, long int selfDid, const std::vector<uint8_t> &X3DH_initMessage)
DR<Curve>::DR(lime::Db *localStorage, const DRChainKey &SK, const SharedADBuffer &AD, const X<Curve> &peerPublicKey, long int peerDid, long int selfDid, const std::vector<uint8_t> &X3DH_initMessage, bctbx_rng_context_t *RNG_context)
:m_DHr{peerPublicKey},m_DHr_valid{true}, m_DHs{},m_RK(SK),m_CKs{},m_CKr{},m_Ns(0),m_Nr(0),m_PN(0),m_sharedAD(AD),m_mkskipped{},
m_RNG{bctbx_rng_context_new()},m_dbSessionId{0},m_usedNr{0},m_usedDHid{0},m_localStorage{localStorage},m_dirty{DRSessionDbStatus::dirty},m_peerDid{peerDid}, m_db_Uid{selfDid},
m_RNG{RNG_context},m_dbSessionId{0},m_usedNr{0},m_usedDHid{0},m_localStorage{localStorage},m_dirty{DRSessionDbStatus::dirty},m_peerDid{peerDid}, m_db_Uid{selfDid},
m_active_status{true}, m_X3DH_initMessage{X3DH_initMessage}
{
// generate a new self key pair
......@@ -196,9 +196,9 @@ namespace lime {
* @param[in] selfDid Id used in local storage for local user this session shall be attached to
*/
template <typename Curve>
DR<Curve>::DR(lime::Db *localStorage, const DRChainKey &SK, const SharedADBuffer &AD, const KeyPair<X<Curve>> &selfKeyPair, long int peerDid, long int selfDid)
DR<Curve>::DR(lime::Db *localStorage, const DRChainKey &SK, const SharedADBuffer &AD, const KeyPair<X<Curve>> &selfKeyPair, long int peerDid, long int selfDid, bctbx_rng_context_t *RNG_context)
:m_DHr{},m_DHr_valid{false},m_DHs{selfKeyPair},m_RK(SK),m_CKs{},m_CKr{},m_Ns(0),m_Nr(0),m_PN(0),m_sharedAD(AD),m_mkskipped{},
m_RNG{bctbx_rng_context_new()},m_dbSessionId{0},m_usedNr{0},m_usedDHid{0},m_localStorage{localStorage},m_dirty{DRSessionDbStatus::dirty},m_peerDid{peerDid}, m_db_Uid{selfDid},
m_RNG{RNG_context},m_dbSessionId{0},m_usedNr{0},m_usedDHid{0},m_localStorage{localStorage},m_dirty{DRSessionDbStatus::dirty},m_peerDid{peerDid}, m_db_Uid{selfDid},
m_active_status{true}, m_X3DH_initMessage{}
{ }
......@@ -211,9 +211,9 @@ namespace lime {
* @param[in] sessionId row id in the database identifying the session to be loaded
*/
template <typename Curve>
DR<Curve>::DR(lime::Db *localStorage, long sessionId)
DR<Curve>::DR(lime::Db *localStorage, long sessionId, bctbx_rng_context_t *RNG_context)
:m_DHr{},m_DHr_valid{true},m_DHs{},m_RK{},m_CKs{},m_CKr{},m_Ns(0),m_Nr(0),m_PN(0),m_sharedAD{},m_mkskipped{},
m_RNG{bctbx_rng_context_new()},m_dbSessionId{sessionId},m_usedNr{0},m_usedDHid{0},m_localStorage{localStorage},m_dirty{DRSessionDbStatus::clean},m_peerDid{0}, m_db_Uid{0},
m_RNG{RNG_context},m_dbSessionId{sessionId},m_usedNr{0},m_usedDHid{0},m_localStorage{localStorage},m_dirty{DRSessionDbStatus::clean},m_peerDid{0}, m_db_Uid{0},
m_active_status{false}, m_X3DH_initMessage{}
{
session_load();
......@@ -225,7 +225,6 @@ namespace lime {
bctbx_clean(m_RK.data(), m_RK.size());
bctbx_clean(m_CKs.data(), m_CKs.size());
bctbx_clean(m_CKr.data(), m_CKr.size());
bctbx_rng_context_free(m_RNG);
}
/**
......
......@@ -95,9 +95,9 @@ namespace lime {
public:
DR() = delete; // make sure the Double Ratchet is not initialised without parameters
DR(lime::Db *localStorage, const DRChainKey &SK, const SharedADBuffer &AD, const X<Curve> &peerPublicKey, long int peerDeviceId, long int selfDeviceId, const std::vector<uint8_t> &X3DH_initMessage); // call to initialise a session for sender: we have Shared Key and peer Public key
DR(lime::Db *localStorage, const DRChainKey &SK, const SharedADBuffer &AD, const KeyPair<X<Curve>> &selfKeyPair, long int peerDeviceId, long int selfDeviceId); // call at initialisation of a session for receiver: we have Share Key and self key pair
DR(lime::Db *localStorage, long sessionId); // load session from DB
DR(lime::Db *localStorage, const DRChainKey &SK, const SharedADBuffer &AD, const X<Curve> &peerPublicKey, long int peerDeviceId, long int selfDeviceId, const std::vector<uint8_t> &X3DH_initMessage, bctbx_rng_context_t *RNG_context); // call to initialise a session for sender: we have Shared Key and peer Public key
DR(lime::Db *localStorage, const DRChainKey &SK, const SharedADBuffer &AD, const KeyPair<X<Curve>> &selfKeyPair, long int peerDeviceId, long int selfDeviceId, bctbx_rng_context_t *RNG_context); // call at initialisation of a session for receiver: we have Share Key and self key pair
DR(lime::Db *localStorage, long sessionId, bctbx_rng_context_t *RNG_context); // load session from DB
DR(DR<Curve> &a) = delete; // can't copy a session, force usage of shared pointers
DR<Curve> &operator=(DR<Curve> &a) = delete; // can't copy a session
~DR();
......
......@@ -798,7 +798,7 @@ void Lime<Curve>::cache_DR_sessions(std::vector<recipientInfos<Curve>> &internal
auto sessionId = r.get<int>(0);
auto peerDeviceId = r.get<string>(1);
auto DRsession = std::make_shared<DR<Curve>>(m_localStorage.get(), sessionId); // load session from local storage
auto DRsession = std::make_shared<DR<Curve>>(m_localStorage.get(), sessionId, m_RNG); // load session from local storage
requestedDevices[peerDeviceId] = DRsession; // store found session in a our temp container
m_DR_sessions_cache[peerDeviceId] = DRsession; // session is also stored in cache
}
......@@ -866,7 +866,7 @@ void Lime<Curve>::get_DRSessions(const std::string &senderDeviceId, const long i
for (const auto &sessionId : rs) {
/* load session in cache DRSessions */
DRSessions.push_back(make_shared<DR<Curve>>(m_localStorage.get(), sessionId)); // load session from cache
DRSessions.push_back(make_shared<DR<Curve>>(m_localStorage.get(), sessionId, m_RNG)); // load session from cache
}
};
......
......@@ -156,7 +156,7 @@ namespace lime {
if (peerBundle.haveOPk) {
m_DR_sessions_cache.erase(peerBundle.deviceId); // will just do nothing if this peerDeviceId is not in cache
}
m_DR_sessions_cache.emplace(peerBundle.deviceId, make_shared<DR<Curve>>(m_localStorage.get(), SK, AD, peerBundle.SPk, peerDid, m_db_Uid, X3DH_initMessage)); // will just do nothing if this peerDeviceId is already in cache
m_DR_sessions_cache.emplace(peerBundle.deviceId, make_shared<DR<Curve>>(m_localStorage.get(), SK, AD, peerBundle.SPk, peerDid, m_db_Uid, X3DH_initMessage, m_RNG)); // will just do nothing if this peerDeviceId is already in cache
BCTBX_SLOGI<<"X3DH created session with device "<<peerBundle.deviceId;
}
......@@ -249,7 +249,7 @@ namespace lime {
long int peerDid=0;
peerDid = store_peerDevice(senderDeviceId, peerIk);
auto DRSession = make_shared<DR<Curve>>(m_localStorage.get(), SK, AD, SPk, peerDid, m_db_Uid);
auto DRSession = make_shared<DR<Curve>>(m_localStorage.get(), SK, AD, SPk, peerDid, m_db_Uid, m_RNG);
bctbx_clean(SPk.privateKey().data(), SPk.privateKey().size());
return DRSession;
......
......@@ -156,7 +156,7 @@ std::vector<std::string> messages_pattern = {
* if fileName doesn't exists as a DB, it will be created, caller shall then delete it if needed
*/
template <typename Curve>
void dr_sessionsInit(std::shared_ptr<DR<Curve>> &alice, std::shared_ptr<DR<Curve>> &bob, std::shared_ptr<lime::Db> &localStorageAlice, std::shared_ptr<lime::Db> &localStorageBob, std::string dbFilenameAlice, std::string dbFilenameBob, bool initStorage) {
void dr_sessionsInit(std::shared_ptr<DR<Curve>> &alice, std::shared_ptr<DR<Curve>> &bob, std::shared_ptr<lime::Db> &localStorageAlice, std::shared_ptr<lime::Db> &localStorageBob, std::string dbFilenameAlice, std::string dbFilenameBob, bool initStorage, bctbx_rng_context_t *RNG_context) {
if (initStorage==true) {
// create or load Db
localStorageAlice = std::make_shared<lime::Db>(dbFilenameAlice);
......@@ -193,8 +193,8 @@ void dr_sessionsInit(std::shared_ptr<DR<Curve>> &alice, std::shared_ptr<DR<Curve
// create DR sessions
std::vector<uint8_t> X3DH_initMessage{};
alice = std::make_shared<DR<Curve>>(localStorageAlice.get(), SK, AD, bobKeyPair.publicKey(), aliceDid, aliceUid, X3DH_initMessage);
bob = std::make_shared<DR<Curve>>(localStorageBob.get(), SK, AD, bobKeyPair, bobDid, bobUid);
alice = std::make_shared<DR<Curve>>(localStorageAlice.get(), SK, AD, bobKeyPair.publicKey(), aliceDid, aliceUid, X3DH_initMessage, RNG_context);
bob = std::make_shared<DR<Curve>>(localStorageBob.get(), SK, AD, bobKeyPair, bobDid, bobUid, RNG_context);
}
......@@ -206,7 +206,7 @@ void dr_sessionsInit(std::shared_ptr<DR<Curve>> &alice, std::shared_ptr<DR<Curve
* createdDBfiles is filled with all filenames of DB created to allow easy deletion
*/
template <typename Curve>
void dr_devicesInit(std::string dbBaseFilename, std::vector<std::vector<std::vector<std::vector<sessionDetails<Curve>>>>> &users, std::vector<std::string> &usernames, std::vector<std::string> &createdDBfiles) {
void dr_devicesInit(std::string dbBaseFilename, std::vector<std::vector<std::vector<std::vector<sessionDetails<Curve>>>>> &users, std::vector<std::string> &usernames, std::vector<std::string> &createdDBfiles, bctbx_rng_context_t *RNG_context) {
createdDBfiles.clear();
/* each device must have a db, produce filename for them from provided base name and given username */
for (size_t i=0; i<users.size(); i++) { // loop on users
......@@ -243,11 +243,11 @@ void dr_devicesInit(std::string dbBaseFilename, std::vector<std::vector<std::vec
for (size_t i=0; i<users.size(); i++) { // loop on users
for (size_t j=0; j<users[i].size(); j++) { // loop on devices
for (size_t j_fw=j+1; j_fw<users[i].size(); j_fw++) { // loop on the rest of our devices
dr_sessionsInit(users[i][j][i][j_fw].DRSession, users[i][j_fw][i][j].DRSession, users[i][j][i][j_fw].localStorage, users[i][j_fw][i][j].localStorage, " ", " ", false);
dr_sessionsInit(users[i][j][i][j_fw].DRSession, users[i][j_fw][i][j].DRSession, users[i][j][i][j_fw].localStorage, users[i][j_fw][i][j].localStorage, " ", " ", false, RNG_context);
}
for (size_t i_fw=i+1; i_fw<users.size(); i_fw++) { // loop on the rest of users
for (size_t j_fw=0; j_fw<users[i].size(); j_fw++) { // loop on the rest of devices
dr_sessionsInit(users[i][j][i_fw][j_fw].DRSession, users[i_fw][j_fw][i][j].DRSession, users[i][j][i_fw][j_fw].localStorage, users[i_fw][j_fw][i][j].localStorage, " ", " ", false);
dr_sessionsInit(users[i][j][i_fw][j_fw].DRSession, users[i_fw][j_fw][i][j].DRSession, users[i][j][i_fw][j_fw].localStorage, users[i_fw][j_fw][i][j].localStorage, " ", " ", false, RNG_context);
}
}
}
......@@ -487,12 +487,12 @@ int wait_for(belle_sip_stack_t*s1,int* counter,int value,int timeout) {
// template instanciation
#ifdef EC25519_ENABLED
template void dr_sessionsInit<C255>(std::shared_ptr<DR<C255>> &alice, std::shared_ptr<DR<C255>> &bob, std::shared_ptr<lime::Db> &localStorageAlice, std::shared_ptr<lime::Db> &localStorageBob, std::string dbFilenameAlice, std::string dbFilenameBob, bool initStorage);
template void dr_devicesInit<C255>(std::string dbBaseFilename, std::vector<std::vector<std::vector<std::vector<sessionDetails<C255>>>>> &users, std::vector<std::string> &usernames, std::vector<std::string> &createdDBfiles);
template void dr_sessionsInit<C255>(std::shared_ptr<DR<C255>> &alice, std::shared_ptr<DR<C255>> &bob, std::shared_ptr<lime::Db> &localStorageAlice, std::shared_ptr<lime::Db> &localStorageBob, std::string dbFilenameAlice, std::string dbFilenameBob, bool initStorage, bctbx_rng_context_t *RNG_context);
template void dr_devicesInit<C255>(std::string dbBaseFilename, std::vector<std::vector<std::vector<std::vector<sessionDetails<C255>>>>> &users, std::vector<std::string> &usernames, std::vector<std::string> &createdDBfiles, bctbx_rng_context_t *RNG_context);
#endif
#ifdef EC448_ENABLED
template void dr_sessionsInit<C448>(std::shared_ptr<DR<C448>> &alice, std::shared_ptr<DR<C448>> &bob, std::shared_ptr<lime::Db> &localStorageAlice, std::shared_ptr<lime::Db> &localStorageBob, std::string dbFilenameAlice, std::string dbFilenameBob, bool initStorage);
template void dr_devicesInit<C448>(std::string dbBaseFilename, std::vector<std::vector<std::vector<std::vector<sessionDetails<C448>>>>> &users, std::vector<std::string> &usernames, std::vector<std::string> &createdDBfiles);
template void dr_sessionsInit<C448>(std::shared_ptr<DR<C448>> &alice, std::shared_ptr<DR<C448>> &bob, std::shared_ptr<lime::Db> &localStorageAlice, std::shared_ptr<lime::Db> &localStorageBob, std::string dbFilenameAlice, std::string dbFilenameBob, bool initStorage, bctbx_rng_context_t *RNG_context);
template void dr_devicesInit<C448>(std::string dbBaseFilename, std::vector<std::vector<std::vector<std::vector<sessionDetails<C448>>>>> &users, std::vector<std::string> &usernames, std::vector<std::string> &createdDBfiles, bctbx_rng_context_t *RNG_context);
#endif
......
......@@ -45,7 +45,7 @@ extern uint16_t OPkInitialBatchSize;
* if fileName doesn't exists as a DB, it will be created, caller shall then delete it if needed
*/
template <typename Curve>
void dr_sessionsInit(std::shared_ptr<DR<Curve>> &alice, std::shared_ptr<DR<Curve>> &bob, std::shared_ptr<lime::Db> &localStorageAlice, std::shared_ptr<lime::Db> &localStorageBob, std::string dbFilenameAlice, std::string dbFilenameBob, bool initStorage=true);
void dr_sessionsInit(std::shared_ptr<DR<Curve>> &alice, std::shared_ptr<DR<Curve>> &bob, std::shared_ptr<lime::Db> &localStorageAlice, std::shared_ptr<lime::Db> &localStorageBob, std::string dbFilenameAlice, std::string dbFilenameBob, bool initStorage, bctbx_rng_context_t *RNG_context);
/* non efficient but used friendly structure to store all details about a session */
......@@ -73,7 +73,7 @@ struct sessionDetails {
* createdDBfiles is filled with all filenames of DB created to allow easy deletion
*/
template <typename Curve>
void dr_devicesInit(std::string dbBaseFilename, std::vector<std::vector<std::vector<std::vector<sessionDetails<Curve>>>>> &users, std::vector<std::string> &usernames, std::vector<std::string> &createdDBfiles);
void dr_devicesInit(std::string dbBaseFilename, std::vector<std::vector<std::vector<std::vector<sessionDetails<Curve>>>>> &users, std::vector<std::string> &usernames, std::vector<std::string> &createdDBfiles, bctbx_rng_context_t *RNG_context);
/* return true if the message buffer is a valid DR message holding a X3DH init one in its header */
bool DR_message_holdsX3DHInit(std::vector<uint8_t> &message);
......@@ -135,12 +135,12 @@ int wait_for(belle_sip_stack_t*s1,int* counter,int value,int timeout);
// template instanciation are done in lime-tester-utils.cpp
#ifdef EC25519_ENABLED
extern template void dr_sessionsInit<C255>(std::shared_ptr<DR<C255>> &alice, std::shared_ptr<DR<C255>> &bob, std::shared_ptr<lime::Db> &localStorageAlice, std::shared_ptr<lime::Db> &localStorageBob, std::string dbFilenameAlice, std::string dbFilenameBob, bool initStorage);
extern template void dr_devicesInit<C255>(std::string dbBaseFilename, std::vector<std::vector<std::vector<std::vector<sessionDetails<C255>>>>> &users, std::vector<std::string> &usernames, std::vector<std::string> &createdDBfiles);
extern template void dr_sessionsInit<C255>(std::shared_ptr<DR<C255>> &alice, std::shared_ptr<DR<C255>> &bob, std::shared_ptr<lime::Db> &localStorageAlice, std::shared_ptr<lime::Db> &localStorageBob, std::string dbFilenameAlice, std::string dbFilenameBob, bool initStorage, bctbx_rng_context_t *RNG_context);
extern template void dr_devicesInit<C255>(std::string dbBaseFilename, std::vector<std::vector<std::vector<std::vector<sessionDetails<C255>>>>> &users, std::vector<std::string> &usernames, std::vector<std::string> &createdDBfiles, bctbx_rng_context_t *RNG_context);
#endif
#ifdef EC448_ENABLED
extern template void dr_sessionsInit<C448>(std::shared_ptr<DR<C448>> &alice, std::shared_ptr<DR<C448>> &bob, std::shared_ptr<lime::Db> &localStorageAlice, std::shared_ptr<lime::Db> &localStorageBob, std::string dbFilenameAlice, std::string dbFilenameBob, bool initStorage);
extern template void dr_devicesInit<C448>(std::string dbBaseFilename, std::vector<std::vector<std::vector<std::vector<sessionDetails<C448>>>>> &users, std::vector<std::string> &usernames, std::vector<std::string> &createdDBfiles);
extern template void dr_sessionsInit<C448>(std::shared_ptr<DR<C448>> &alice, std::shared_ptr<DR<C448>> &bob, std::shared_ptr<lime::Db> &localStorageAlice, std::shared_ptr<lime::Db> &localStorageBob, std::string dbFilenameAlice, std::string dbFilenameBob, bool initStorage, bctbx_rng_context_t *RNG_context);
extern template void dr_devicesInit<C448>(std::string dbBaseFilename, std::vector<std::vector<std::vector<std::vector<sessionDetails<C448>>>>> &users, std::vector<std::string> &usernames, std::vector<std::string> &createdDBfiles, bctbx_rng_context_t *RNG_context);
#endif
// the test server has only one user registered but accept commands from any users using this credentials
......
......@@ -39,6 +39,18 @@
using namespace::std;
using namespace::lime;
static bctbx_rng_context_t *RNG_context=nullptr;
static int start_RNG_before_all(void) {
RNG_context = bctbx_rng_context_new();
return 0;
}
static int stop_RNG_after_all(void) {
bctbx_rng_context_free(RNG_context);
return 0;
}
/**
* @param[in] period altern sended each <period> messages (sequence will anyways always start with alice send - bob receive - bob send)
* @param[in] skip_period same than above but for reception skipping: at each begining of skip_period, skip reception of skip_length messages
......@@ -61,7 +73,7 @@ static void dr_skippedMessages_basic_test(const uint8_t period=1, const uint8_t
remove(bobFilename.data());
// create sessions
lime_tester::dr_sessionsInit(alice, bob, aliceLocalStorage, bobLocalStorage, aliceFilename, bobFilename);
lime_tester::dr_sessionsInit(alice, bob, aliceLocalStorage, bobLocalStorage, aliceFilename, bobFilename, true, RNG_context);
std::vector<std::vector<uint8_t>> cipher;
std::vector<std::vector<recipientInfos<Curve>>> recipients;
std::vector<uint8_t> messageSender; // hold status of message: 0 not sent, 1 sent by Alice, 2 sent by Bob, 3 received
......@@ -218,7 +230,7 @@ static void dr_long_exchange_test(uint8_t period=1, std::string db_filename="dr_
aliceFilename.append(".alice.sqlite3");
bobFilename.append(".bob.sqlite3");
// create sessions
lime_tester::dr_sessionsInit(alice, bob, aliceLocalStorage, bobLocalStorage, aliceFilename, bobFilename);
lime_tester::dr_sessionsInit(alice, bob, aliceLocalStorage, bobLocalStorage, aliceFilename, bobFilename, true, RNG_context);
std::vector<uint8_t> aliceCipher, bobCipher;
bool aliceSender=true;
......@@ -246,7 +258,7 @@ static void dr_long_exchange_test(uint8_t period=1, std::string db_filename="dr_
/* destroy and reload bob sessions */
auto bobSessionId=bob->dbSessionId();
bob = nullptr; // release and destroy bob DR context
bob = make_shared<DR<Curve>>(bobLocalStorage.get(), bobSessionId);
bob = make_shared<DR<Curve>>(bobLocalStorage.get(), bobSessionId, RNG_context);
}
} else {
// bob replies
......@@ -270,7 +282,7 @@ static void dr_long_exchange_test(uint8_t period=1, std::string db_filename="dr_
/* destroy and reload alice sessions */
auto aliceSessionId=alice->dbSessionId();
alice = nullptr; // release and destroy alice DR context
alice = make_shared<DR<Curve>>(aliceLocalStorage.get(), aliceSessionId);
alice = make_shared<DR<Curve>>(aliceLocalStorage.get(), aliceSessionId, RNG_context);
}
}
}
......@@ -312,7 +324,7 @@ static void dr_simple_exchange(std::shared_ptr<DR<Curve>> &DRsessionAlice, std::
std::shared_ptr<lime::Db> &localStorageAlice, std::shared_ptr<lime::Db> &localStorageBob,
std::string &filenameAlice, std::string &filenameBob) {
// create sessions: alice sender, bob receiver
lime_tester::dr_sessionsInit(DRsessionAlice, DRsessionBob, localStorageAlice, localStorageBob, filenameAlice, filenameBob);
lime_tester::dr_sessionsInit(DRsessionAlice, DRsessionBob, localStorageAlice, localStorageBob, filenameAlice, filenameBob, true, RNG_context);
std::vector<uint8_t> aliceCipher, bobCipher;
// alice encrypt a message
......@@ -393,7 +405,7 @@ static void dr_multidevice_basic_test(std::string db_filename) {
/* init and instanciate, session will be then found in a 4 dimensional vector indexed this way : [self user id][self device id][peer user id][peer device id] */
std::vector<std::string> created_db_files{};
lime_tester::dr_devicesInit(db_filename, users, usernames, created_db_files);
lime_tester::dr_devicesInit(db_filename, users, usernames, created_db_files, RNG_context);
/* Send a message from alice.dev0 to all bob device(and copy to alice devices too) */
std::vector<recipientInfos<Curve>> recipients;
......@@ -588,8 +600,8 @@ static test_t tests[] = {
test_suite_t lime_double_ratchet_test_suite = {
"Double Ratchet",
NULL,
NULL,
start_RNG_before_all,
stop_RNG_after_all,
NULL,
NULL,
sizeof(tests) / sizeof(tests[0]),
......
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