/* * Tencent is pleased to support the open source community by making * MMKV available. * * Copyright (C) 2020 THL A29 Limited, a Tencent company. * All rights reserved. * * Licensed under the BSD 3-Clause License (the "License"); you may not use * this file except in compliance with the License. You may obtain a copy of * the License at * * https://opensource.org/licenses/BSD-3-Clause * * 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. */ #include #include "CodedInputData.h" #include "CodedOutputData.h" #include "KeyValueHolder.h" #include "PBUtility.h" #include "aes/AESCrypt.h" #include "crc32/Checksum.h" #include "MemoryFile.h" #include "MMKVMetaInfo.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; using namespace mmkv; static const string KeyNotExist = "KeyNotExist"; void testBool(MMKV *mmkv) { auto ret = mmkv->set(true, "bool"); assert(ret); auto value = mmkv->getBool("bool"); assert(value); value = mmkv->getBool(KeyNotExist); assert(!value); value = mmkv->getBool(KeyNotExist, true); assert(value); printf("test bool: passed\n"); } void testInt32(MMKV *mmkv) { auto ret = mmkv->set(numeric_limits::max(), "int32"); assert(ret); auto value = mmkv->getInt32("int32"); assert(value == numeric_limits::max()); value = mmkv->getInt32(KeyNotExist); assert(value == 0); value = mmkv->getInt32(KeyNotExist, -1); assert(value == -1); printf("test int32: passed\n"); } void testUInt32(MMKV *mmkv) { auto ret = mmkv->set(numeric_limits::max(), "uint32"); assert(ret); auto value = mmkv->getUInt32("uint32"); assert(value == numeric_limits::max()); value = mmkv->getUInt32(KeyNotExist); assert(value == 0); value = mmkv->getUInt32(KeyNotExist, -1); assert(value == -1); printf("test uint32: passed\n"); } void testInt64(MMKV *mmkv) { auto ret = mmkv->set(numeric_limits::min(), "int64"); assert(ret); auto value = mmkv->getInt64("int64"); assert(value == numeric_limits::min()); value = mmkv->getInt64(KeyNotExist); assert(value == 0); value = mmkv->getInt64(KeyNotExist, -1); assert(value == -1); printf("test int64: passed\n"); } void testUInt64(MMKV *mmkv) { auto ret = mmkv->set(numeric_limits::max(), "uint64"); assert(ret); auto value = mmkv->getUInt64("uint64"); assert(value == numeric_limits::max()); value = mmkv->getUInt64(KeyNotExist); assert(value == 0); value = mmkv->getUInt64(KeyNotExist, -1); assert(value == -1); printf("test uint64: passed\n"); } template bool EqualWithAccuracy(T value1, T value2, T accuracy) { return fabs(value1 - value2) <= accuracy; } void testFloat(MMKV *mmkv) { auto ret = mmkv->set(numeric_limits::max(), "float"); assert(ret); auto value = mmkv->getFloat("float"); assert(EqualWithAccuracy(value, numeric_limits::max(), 0.001f)); value = mmkv->getFloat(KeyNotExist); assert(EqualWithAccuracy(value, 0.0f, 0.001f)); value = mmkv->getFloat(KeyNotExist, -1.0f); assert(EqualWithAccuracy(value, -1.0f, 0.001f)); printf("test float: passed\n"); } void testDouble(MMKV *mmkv) { auto ret = mmkv->set(numeric_limits::max(), "double"); assert(ret); auto value = mmkv->getDouble("double"); assert(EqualWithAccuracy(value, numeric_limits::max(), 0.001)); value = mmkv->getDouble(KeyNotExist); assert(EqualWithAccuracy(value, 0.0, 0.001)); value = mmkv->getDouble(KeyNotExist, -1.0); assert(EqualWithAccuracy(value, -1.0, 0.001)); printf("test double: passed\n"); } void testString(MMKV *mmkv) { string str = "Hello 2018 world cup 世界杯"; auto ret = mmkv->set(str, "string"); assert(ret); string value; ret = mmkv->getString("string", value); assert(ret && str == value); const char *cString = "Hello 2022 world cup 世界杯"; ret = mmkv->set(cString, "cstring"); assert(ret); ret = mmkv->getString("cstring", value); assert(ret && value == cString); ret = mmkv->getString(KeyNotExist, value); assert(!ret); printf("test string: passed\n"); } void testBytes(MMKV *mmkv) { string str = "Hello 2018 world cup 世界杯"; MMBuffer buffer((void *) str.data(), str.length(), MMBufferNoCopy); auto ret = mmkv->set(buffer, "bytes"); assert(ret); auto value = mmkv->getBytes("bytes"); assert(value.length() == buffer.length() && memcmp(value.getPtr(), buffer.getPtr(), value.length()) == 0); value = mmkv->getBytes(KeyNotExist); assert(value.length() == 0); printf("test bytes: passed\n"); } void testVector(MMKV *mmkv) { vector v = {"1", "0", "2", "4"}; auto ret = mmkv->set(v, "vector"); assert(ret); vector value; ret = mmkv->getVector("vector", value); assert(ret && value == v); printf("test vector: passed\n"); } namespace { // Test-only access to synthesize a large logical value without allocating it. MMKVMap *&expirationTestDictionary(MMKV &kv); template struct ExpirationDictionaryAccess { friend MMKVMap *&expirationTestDictionary(MMKV &kv) { return kv.*Member; } }; template struct ExpirationDictionaryAccess<&MMKV::m_dic>; } void testExpirationMigrationAggregateLimit() { const string id = "expiration_aggregate_limit_test"; auto kv = MMKV::mmkvWithID(id); kv->clearAll(); assert(kv->set("before", "k")); assert(kv->enableCompareBeforeSet()); auto &holder = expirationTestDictionary(*kv)->at("k"); const auto originalSize = holder.valueSize; // With a one-byte key, migration adds 11 bytes per entry and a four-byte // map placeholder. UINT32_MAX total is one byte beyond the temporary // protobuf buffer's limit; the next case also exceeds the file-size limit. for (uint32_t extra = 0; extra <= 1; ++extra) { holder.valueSize = numeric_limits::max() - 15 + extra; const auto enabled = kv->enableAutoKeyExpire(60); holder.valueSize = originalSize; assert(!enabled); assert(!kv->isExpirationEnabled()); assert(kv->isCompareBeforeSetEnabled()); string value; assert(kv->getString("k", value) && value == "before"); } kv->close(); kv = MMKV::mmkvWithID(id); string value; assert(kv->getString("k", value) && value == "before"); assert(!kv->isExpirationEnabled()); kv->close(); printf("test expiration migration aggregate limit: passed\n"); } void testOversizedKey(MMKV *mmkv) { // Keys at or below 65531 bytes should work normally { string key(65531, 'A'); auto ret = mmkv->set("V", key); assert(ret); string out; ret = mmkv->getString(key, out); assert(ret && out == "V"); // The key still fits, but a two-byte value-length prefix would overflow // KeyValueHolder::computedKVSize. Reject the update before writing it. assert(!mmkv->set(string(127, 'V'), key)); assert(mmkv->getString(key, out) && out == "V"); mmkv->removeValueForKey(key); assert(!mmkv->set(string(127, 'V'), key)); assert(!mmkv->containsKey(key)); } // Keys at 65532 bytes and above must be rejected (would overflow uint16_t fields) { string key(65532, 'B'); auto ret = mmkv->set("V", key); assert(!ret); string out; ret = mmkv->getString(key, out); assert(!ret); } { string key(70000, 'C'); auto ret = mmkv->set("V", key); assert(!ret); } { auto expiring = MMKV::mmkvWithID("oversized_expiration_transition_test"); expiring->clearAll(); string key(65531, 'D'); string storedValue(124, 'V'); assert(expiring->set(storedValue, key)); assert(!expiring->enableAutoKeyExpire()); assert(!expiring->isExpirationEnabled()); string out; assert(expiring->getString(key, out) && out == storedValue); expiring->clearAll(); } printf("test oversized key: passed\n"); } void testOversizedValue(MMKV *mmkv) { uint8_t sentinel = 0; MMBuffer boundary(&sentinel, static_cast(numeric_limits::max()) - 5, MMBufferNoCopy); string value; mmkv->clearAll(); assert(mmkv->set("before", "value")); assert(!mmkv->set(boundary, "value")); assert(mmkv->getString("value", value) && value == "before"); assert(mmkv->set("after", "value")); assert(mmkv->getString("value", value) && value == "after"); if (numeric_limits::max() > numeric_limits::max()) { MMBuffer oversized(&sentinel, static_cast(numeric_limits::max()) + 1, MMBufferNoCopy); mmkv->clearAll(); assert(!mmkv->set(oversized, "oversized-append")); assert(mmkv->set("after", "value")); assert(mmkv->getString("value", value) && value == "after"); mmkv->clearAll(); assert(mmkv->set("before", "value")); assert(!mmkv->set(oversized, "value")); assert(mmkv->getString("value", value) && value == "before"); assert(mmkv->set("after", "value")); assert(mmkv->getString("value", value) && value == "after"); } auto expiring = MMKV::mmkvWithID("oversized_expiring_value_test"); expiring->clearAll(); assert(expiring->enableAutoKeyExpire()); if (numeric_limits::max() > numeric_limits::max()) { MMBuffer oversized(&sentinel, static_cast(numeric_limits::max()) + 1, MMBufferNoCopy); assert(!expiring->set(oversized, "value", 60)); } assert(expiring->set("after", "value", 60)); assert(expiring->getString("value", value) && value == "after"); expiring->clearAll(); printf("test oversized value: passed\n"); } void testFileValidation(const string &rootDir) { const string validationID = "file_validation_test"; MMKV::removeStorage(validationID); auto mmkv = MMKV::mmkvWithID(validationID); mmkv->clearAll(); assert(mmkv->set("value", "key")); mmkv->sync(MMKV_SYNC); mmkv->close(); // Current-format files keep their actual size in metadata, not in the // first four data bytes. assert(MMKV::isFileValid(validationID)); auto crcPath = rootDir + "/" + validationID + ".crc"; assert(truncate(crcPath.c_str(), sizeof(uint32_t)) == 0); assert(!MMKV::isFileValid(validationID)); assert(MMKV::removeStorage(validationID)); printf("test file validation: passed\n"); } void testTinyFileRecovery(const string &rootDir) { class RecoverHandler final : public MMKVHandler { public: size_t fileLengthErrorCount = 0; MMKVRecoverStrategic onMMKVFileLengthError(const string &) override { fileLengthErrorCount++; return OnErrorRecover; } } handler; MMKV::registerHandler(&handler); for (size_t fileSize = 1; fileSize <= Fixed32Size; fileSize++) { auto mmapID = "tiny_file_" + to_string(fileSize); MMKV::removeStorage(mmapID); auto mmkv = MMKV::mmkvWithID(mmapID); mmkv->clearAll(); assert(mmkv->set("value", "key")); mmkv->sync(MMKV_SYNC); mmkv->close(); auto path = rootDir + "/" + mmapID; assert(truncate(path.c_str(), static_cast(fileSize)) == 0); assert(!MMKV::isFileValid(mmapID)); auto readOnly = MMKV::mmkvWithID(mmapID, MMKV_SINGLE_PROCESS | MMKV_READ_ONLY); assert(readOnly && readOnly->count() == 0); assert(handler.fileLengthErrorCount == fileSize); readOnly->close(); assert(MMKV::removeStorage(mmapID)); } MMKV::unRegisterHandler(); printf("test tiny-file recovery: passed\n"); } void testExpirationOverflow() { auto mmkv = MMKV::mmkvWithID("expiration_overflow_test"); mmkv->clearAll(); assert(mmkv->enableAutoKeyExpire(numeric_limits::max())); auto ret = mmkv->set(true, "expiration_overflow_auto"); assert(ret); assert(mmkv->getBool("expiration_overflow_auto")); ret = mmkv->set("manual", "expiration_overflow_manual", numeric_limits::max()); assert(ret); string value; ret = mmkv->getString("expiration_overflow_manual", value); assert(ret && value == "manual"); string bytes = "bytes"; MMBuffer buffer((void *) bytes.data(), bytes.length(), MMBufferNoCopy); ret = mmkv->set(buffer, "expiration_overflow_bytes", numeric_limits::max()); assert(ret); auto out = mmkv->getBytes("expiration_overflow_bytes"); assert(out.length() == buffer.length() && memcmp(out.getPtr(), buffer.getPtr(), out.length()) == 0); assert(mmkv->count(true) == 3); mmkv->clearAll(); printf("test expiration overflow: passed\n"); } void testCodedOutputBounds() { array storage; storage.fill(0xA5); CodedOutputData fixedOutput(storage.data(), 3); bool rejected = false; try { fixedOutput.writeRawLittleEndian32(0x12345678); } catch (const out_of_range &) { rejected = true; } assert(rejected && fixedOutput.getPosition() == 0 && storage[0] == 0xA5); storage.fill(0xA5); CodedOutputData shortVarint(storage.data(), 1); rejected = false; try { shortVarint.writeUInt32(128); } catch (const out_of_range &) { rejected = true; } assert(rejected && shortVarint.getPosition() == 0 && storage[0] == 0xA5); constexpr array values32 = {0, 127, 128, 16383, 16384, numeric_limits::max()}; const vector> expected32 = { {0x00}, {0x7f}, {0x80, 0x01}, {0xff, 0x7f}, {0x80, 0x80, 0x01}, {0xff, 0xff, 0xff, 0xff, 0x0f}, }; for (size_t index = 0; index < values32.size(); index++) { storage.fill(0); CodedOutputData output(storage.data(), storage.size()); output.writeUInt32(values32[index]); assert(output.getPosition() == expected32[index].size()); assert(equal(expected32[index].begin(), expected32[index].end(), storage.begin())); } constexpr array wideLength = {0x80, 0x80, 0x80, 0x80, 0x08}; CodedInputData wideInput(wideLength.data(), wideLength.size()); rejected = false; try { wideInput.readData(false); } catch (const out_of_range &) { rejected = true; } assert(rejected); storage.fill(0); CodedOutputData signedOutput(storage.data(), storage.size()); signedOutput.writeInt32(-1); CodedInputData signedInput(storage.data(), signedOutput.getPosition()); assert(signedInput.readInt32() == -1); constexpr array values64 = {0, 127, 128, 16383, 16384, numeric_limits::max()}; const vector> expected64 = { {0x00}, {0x7f}, {0x80, 0x01}, {0xff, 0x7f}, {0x80, 0x80, 0x01}, {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01}, }; for (size_t index = 0; index < values64.size(); index++) { storage.fill(0); CodedOutputData output(storage.data(), storage.size()); output.writeUInt64(values64[index]); assert(output.getPosition() == expected64[index].size()); assert(equal(expected64[index].begin(), expected64[index].end(), storage.begin())); } storage.fill(0); CodedOutputData littleEndianOutput(storage.data(), sizeof(uint32_t)); littleEndianOutput.writeRawLittleEndian32(0x12345678); constexpr array littleEndian32 = {0x78, 0x56, 0x34, 0x12}; assert(equal(littleEndian32.begin(), littleEndian32.end(), storage.begin())); const char payload[] = "data"; storage.fill(0xA5); CodedOutputData dataOutput(storage.data(), 4); rejected = false; try { dataOutput.writeData(MMBuffer((void *) payload, 4, MMBufferNoCopy)); } catch (const out_of_range &) { rejected = true; } assert(rejected && dataOutput.getPosition() == 0 && all_of(storage.begin(), storage.end(), [](auto byte) { return byte == 0xA5; })); storage.fill(0xA5); CodedOutputData stringOutput(storage.data(), 4); rejected = false; try { stringOutput.writeString("data"); } catch (const out_of_range &) { rejected = true; } assert(rejected && stringOutput.getPosition() == 0 && all_of(storage.begin(), storage.end(), [](auto byte) { return byte == 0xA5; })); CodedOutputData positionOutput(storage.data(), 4); positionOutput.setPosition(2); rejected = false; try { positionOutput.seek(3); } catch (const out_of_range &) { rejected = true; } assert(rejected && positionOutput.getPosition() == 2); rejected = false; try { positionOutput.setPosition(5); } catch (const out_of_range &) { rejected = true; } assert(rejected && positionOutput.getPosition() == 2); if (numeric_limits::max() > numeric_limits::max()) { storage.fill(0xA5); MMBuffer wideValue(storage.data(), static_cast(numeric_limits::max()) + 1, MMBufferNoCopy); CodedOutputData output(storage.data(), storage.size()); rejected = false; try { output.writeData(wideValue); } catch (const out_of_range &) { rejected = true; } assert(rejected && output.getPosition() == 0 && storage[0] == 0xA5); } if (numeric_limits::max() > numeric_limits::max()) { storage.fill(0xA5); MMBuffer oversized(storage.data(), static_cast(numeric_limits::max()) + 1, MMBufferNoCopy); CodedOutputData output(storage.data(), storage.size()); rejected = false; try { output.writeData(oversized); } catch (const length_error &) { rejected = true; } assert(rejected && output.getPosition() == 0 && storage[0] == 0xA5); } MMBuffer smallBuffer(16); memset(smallBuffer.getPtr(), 0x5A, smallBuffer.length()); MMBuffer limitedBuffer(std::move(smallBuffer), 256); assert(limitedBuffer.length() == 16); printf("test coded output bounds: passed\n"); } void testExpirationAlignment() { auto mmkv = MMKV::mmkvWithID("expiration_alignment_test"); mmkv->clearAll(); assert(mmkv->enableAutoKeyExpire()); for (size_t length = 1; length <= 4; length++) { auto key = "alignment-" + to_string(length); assert(mmkv->set(string(length, 'x'), key, 60)); string value; assert(mmkv->getString(key, value) && value == string(length, 'x')); } assert(mmkv->count(true) == 4); mmkv->clearAll(); #ifndef MMKV_DISABLE_CRYPT const string cryptKey = "alignment-key"; auto encrypted = MMKV::mmkvWithID("expiration_alignment_crypt_test", MMKV_SINGLE_PROCESS, &cryptKey); encrypted->clearAll(); assert(encrypted->enableAutoKeyExpire()); for (size_t length = 1; length <= 4; length++) { auto key = "alignment-" + to_string(length); assert(encrypted->set(string(length, 'x'), key, 60)); string value; assert(encrypted->getString(key, value) && value == string(length, 'x')); } assert(encrypted->count(true) == 4); encrypted->clearAll(); #endif printf("test expiration alignment: passed\n"); } void testArmCRC32() { #if defined(MMKV_USE_ARMV8_CRC32) && !defined(MMKV_OHOS) if (CRC32 != mmkv::armv8_crc32) { return; } array storage = {}; for (size_t index = 0; index < storage.size(); index++) { storage[index] = static_cast(index * 37 + 11); } constexpr array seeds = {0, 1, 0xdeadbeef}; for (auto seed : seeds) { for (size_t offset = 0; offset < 8; offset++) { for (size_t length = 0; length <= 257; length++) { auto ptr = storage.data() + offset; assert(mmkv::armv8_crc32(seed, ptr, length) == ZLIB_CRC32(seed, ptr, length)); } } } printf("test ARM CRC32 alignment: passed\n"); #endif } #ifndef MMKV_DISABLE_CRYPT static bool containsBytes(const unsigned char *storage, size_t storageSize, const uint8_t *value, size_t valueSize) { if (valueSize > storageSize) { return false; } for (size_t offset = 0; offset <= storageSize - valueSize; ++offset) { if (memcmp(storage + offset, value, valueSize) == 0) { return true; } } return false; } void testCryptoRandomAndWipe() { uint8_t firstIV[AES_IV_LEN] = {}; uint8_t secondIV[AES_IV_LEN] = {}; assert(AESCrypt::fillRandomIV(firstIV)); assert(AESCrypt::fillRandomIV(secondIV)); assert(memcmp(firstIV, secondIV, sizeof(firstIV)) != 0); uint8_t key[AES256_KEY_LEN]; iota(begin(key), end(key), 1); alignas(AESCrypt) unsigned char storage[sizeof(AESCrypt)] = {}; auto crypt = new (storage) AESCrypt(key, sizeof(key), nullptr, 0, true); assert(containsBytes(storage, sizeof(storage), key, sizeof(key))); crypt->~AESCrypt(); assert(!containsBytes(storage, sizeof(storage), key, sizeof(key))); const string exactAES128Key = "0123456789abcdef"; auto encrypted = MMKV::mmkvWithID("aes-mode-switch", MMKV_SINGLE_PROCESS, &exactAES128Key); encrypted->clearAll(); assert(encrypted->cryptKey() == exactAES128Key); assert(encrypted->set("payload", "value")); assert(encrypted->reKey(exactAES128Key, true)); string value; assert(encrypted->getString("value", value) && value == "payload"); encrypted->close(); encrypted = MMKV::mmkvWithID("aes-mode-switch", MMKV_SINGLE_PROCESS, &exactAES128Key, nullptr, 0, true); assert(encrypted->getString("value", value) && value == "payload"); encrypted->clearAll(); encrypted->close(); const string transitionKey = "transition-key"; auto transition = MMKV::mmkvWithID("aes-reset-transition"); transition->clearAll(); transition->checkReSetCryptKey(&transitionKey, false); assert(transition->set("encrypted", "value")); assert(transition->getString("value", value) && value == "encrypted"); transition->clearAll(); transition->checkReSetCryptKey(nullptr); assert(transition->set("plain", "value")); assert(transition->getString("value", value) && value == "plain"); transition->clearAll(); transition->checkReSetCryptKey(&transitionKey, true); assert(transition->set("aes256", "value")); assert(transition->getString("value", value) && value == "aes256"); transition->clearAll(); transition->close(); printf("test crypto random and wipe: passed\n"); } #endif void testLongDirectoryWalk(const string &testRoot) { auto baseTemplate = testRoot + "/mmkv-walk-XXXXXX"; vector mutableTemplate(baseTemplate.begin(), baseTemplate.end()); mutableTemplate.push_back('\0'); auto base = mkdtemp(mutableTemplate.data()); assert(base); string path(base); vector> directories; auto currentFD = open(base, O_RDONLY | O_DIRECTORY); assert(currentFD >= 0); constexpr size_t targetPathLength = PATH_MAX - 64; while (path.size() < targetPathLength) { const auto remainingLength = targetPathLength - path.size(); if (remainingLength == 1) { path.push_back('/'); break; } auto nameLength = min(100, remainingLength - 1); string name(nameLength, static_cast('a' + directories.size() % 26)); assert(mkdirat(currentFD, name.c_str(), 0700) == 0); auto childFD = openat(currentFD, name.c_str(), O_RDONLY | O_DIRECTORY); assert(childFD >= 0); directories.emplace_back(currentFD, name); currentFD = childFD; path += "/" + name; } assert(path.size() == targetPathLength); const string filename(100, 'z'); auto fileFD = openat(currentFD, filename.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0600); assert(fileFD >= 0); close(fileFD); vector files; walkInDir(path, WalkFile, [&files](const MMKVPath_t &filePath, WalkType) { files.push_back(filePath); }); assert(files.size() == 1); assert(files.front() == path + "/" + filename); assert(unlinkat(currentFD, filename.c_str(), 0) == 0); close(currentFD); for (auto it = directories.rbegin(); it != directories.rend(); ++it) { assert(unlinkat(it->first, it->second.c_str(), AT_REMOVEDIR) == 0); close(it->first); } assert(rmdir(base) == 0); printf("test long directory walk: passed\n"); } void testMinimalBackupRestore(const string &rootDir) { const string suffixID = "suffix.crc"; const string specialID = "special/id"; auto suffix = MMKV::mmkvWithID(suffixID); auto special = MMKV::mmkvWithID(specialID); suffix->clearAll(); special->clearAll(); assert(suffix->set("before", "value")); assert(special->set("before-special", "value")); const auto suffixPath = rootDir + "/" + suffixID; const auto suffixCRCPath = suffixPath + ".crc"; struct stat beforeAlias = {}; struct stat afterAlias = {}; struct stat beforeCRCAlias = {}; struct stat afterCRCAlias = {}; assert(stat(suffixPath.c_str(), &beforeAlias) == 0); assert(stat(suffixCRCPath.c_str(), &beforeCRCAlias) == 0); const auto rootAlias = rootDir + "/."; assert(MMKV::backupOneToDirectory(suffixID, rootAlias, &rootDir)); assert(MMKV::restoreOneFromDirectory(suffixID, rootAlias, &rootDir)); assert(MMKV::backupAllToDirectory(rootAlias, &rootDir) != 0); assert(MMKV::restoreAllFromDirectory(rootAlias, &rootDir) != 0); assert(stat(suffixPath.c_str(), &afterAlias) == 0); assert(stat(suffixCRCPath.c_str(), &afterCRCAlias) == 0); assert(beforeAlias.st_dev == afterAlias.st_dev && beforeAlias.st_ino == afterAlias.st_ino); assert(beforeCRCAlias.st_dev == afterCRCAlias.st_dev && beforeCRCAlias.st_ino == afterCRCAlias.st_ino); string value; assert(suffix->getString("value", value) && value == "before"); const auto uniqueSuffix = to_string(static_cast(getpid())); const auto backupDir = rootDir + "-backup-" + uniqueSuffix; assert(MMKV::backupAllToDirectory(backupDir, &rootDir) >= 2); assert(suffix->set("after", "value")); assert(special->set("after-special", "value")); assert(MMKV::restoreAllFromDirectory(backupDir, &rootDir) >= 2); assert(suffix->getString("value", value) && value == "before"); assert(special->getString("value", value) && value == "before-special"); const auto invalidDir = rootDir + "-invalid-" + uniqueSuffix; assert(mkPath(invalidDir)); assert(copyFile(backupDir + "/" + suffixID, invalidDir + "/" + suffixID)); auto invalidCRC = invalidDir + "/" + suffixID + ".crc"; auto invalidFD = open(invalidCRC.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0600); assert(invalidFD >= 0); const uint8_t truncatedMetadata = 0; assert(write(invalidFD, &truncatedMetadata, sizeof(truncatedMetadata)) == static_cast(sizeof(truncatedMetadata))); close(invalidFD); assert(suffix->set("unchanged", "value")); assert(!MMKV::restoreOneFromDirectory(suffixID, invalidDir, &rootDir)); assert(suffix->getString("value", value) && value == "unchanged"); assert(copyFile(backupDir + "/" + suffixID + ".crc", invalidCRC)); invalidFD = open((invalidDir + "/" + suffixID).c_str(), O_WRONLY | O_TRUNC); assert(invalidFD >= 0); close(invalidFD); assert(!MMKV::restoreOneFromDirectory(suffixID, invalidDir, &rootDir)); assert(suffix->getString("value", value) && value == "unchanged"); suffix->clearAll(); special->clearAll(); std::filesystem::remove_all(backupDir); std::filesystem::remove_all(invalidDir); printf("test minimal backup and restore hardening: passed\n"); } #ifndef MMKV_DISABLE_CRYPT void testDecryptToPlainKeyLimit() { const string cryptKey = "plain-holder-test"; for (auto valueSize : {128, 300}) { auto kv = MMKV::mmkvWithID("decrypt_plain_key_limit_" + to_string(valueSize), MMKV_SINGLE_PROCESS, &cryptKey); kv->clearAll(); const string key(KeySizeLimit, 'k'); const string value(valueSize, 'v'); assert(kv->set(value, key)); assert(kv->set("intact", "ordinary")); assert(!kv->reKey("")); assert(kv->isEncryptionEnabled()); kv->clearMemoryCache(); string result; assert(kv->getString(key, result) && result == value); assert(kv->getString("ordinary", result) && result == "intact"); assert(kv->removeValueForKey(key)); assert(kv->set(value, key.substr(1))); assert(kv->reKey("")); assert(kv->getString(key.substr(1), result) && result == value); assert(kv->getString("ordinary", result) && result == "intact"); kv->close(); } printf("test decrypt to plain key limit: passed\n"); } #endif void testCachedRestore(const string &rootDir) { const string backupDir = rootDir + "/cached-restore-backup"; const string value(DEFAULT_MMAP_SIZE * 2, 'v'); auto backup = MMKV::nameSpace(backupDir).mmkvWithID("cached-restore"); assert(backup->set(value, "key")); backup->sync(MMKV_SYNC); auto backupSize = backup->totalSize(); backup->close(); for (auto capacity : {DEFAULT_MMAP_SIZE, backupSize * 2}) { MMKVConfig config; config.expectedCapacity = capacity; auto kv = MMKV::mmkvWithID("cached-restore", config); assert(MMKV::restoreOneFromDirectory("cached-restore", backupDir)); string result; assert(kv->getString("key", result) && result == value); assert(kv->set(value, "another")); kv->close(); MMKV::removeStorage("cached-restore"); } backup = MMKV::nameSpace(backupDir).mmkvWithID("invalid-restore-meta"); assert(backup->set(value, "key")); backup->sync(MMKV_SYNC); backup->close(); assert(mkPath(rootDir + "/invalid-restore-meta.crc")); auto invalid = MMKV::mmkvWithID("invalid-restore-meta"); assert(invalid); assert(!MMKV::restoreOneFromDirectory("invalid-restore-meta", backupDir)); invalid->close(); printf("test cached restore: passed\n"); } void testReadOnlyRecovery(const string &rootDir) { for (bool useLastConfirmed : {false, true}) { const string id = useLastConfirmed ? "readonly-confirmed" : "readonly-header"; auto kv = MMKV::mmkvWithID(id); assert(kv->set("intact", "key")); auto goodSize = static_cast(kv->actualSize()); auto badSize = static_cast(kv->totalSize() + 4); kv->sync(MMKV_SYNC); kv->close(); auto fd = open((rootDir + "/" + id + ".crc").c_str(), O_RDWR); assert(fd >= 0); MMKVMetaInfo meta; assert(pread(fd, &meta, sizeof(meta), 0) == sizeof(meta)); meta.m_lastConfirmedMetaInfo.lastActualSize = goodSize; meta.m_lastConfirmedMetaInfo.lastCRCDigest = meta.m_crcDigest; meta.m_actualSize = badSize; assert(pwrite(fd, &meta, sizeof(meta), 0) == sizeof(meta)); close(fd); fd = open((rootDir + "/" + id).c_str(), O_RDWR); assert(fd >= 0); auto headerSize = useLastConfirmed ? badSize : goodSize; assert(pwrite(fd, &headerSize, sizeof(headerSize), 0) == sizeof(headerSize)); close(fd); kv = MMKV::mmkvWithID(id, MMKV_SINGLE_PROCESS | MMKV_READ_ONLY); string result; assert(kv->getString("key", result) && result == "intact"); assert(kv->actualSize() == goodSize); kv->close(); fd = open((rootDir + "/" + id + ".crc").c_str(), O_RDONLY); assert(fd >= 0); MMKVMetaInfo after; assert(pread(fd, &after, sizeof(after), 0) == sizeof(after)); assert(memcmp(&meta, &after, sizeof(meta)) == 0); close(fd); } printf("test readonly recovery: passed\n"); } void testImportRejectedWrites() { auto source = MMKV::mmkvWithID("import-count-source"); source->clearAll(); assert(source->set("kept", "small")); assert(source->set(string(64, 'v'), "large")); for (bool expire : {false, true}) { MMKVConfig config; config.itemSizeLimit = 32; config.enableKeyExpire = expire; auto dest = MMKV::mmkvWithID(expire ? "import-count-expiring" : "import-count-plain", config); dest->clearAll(); assert(dest->importFrom(source) == 1); assert(dest->count() == 1); string value; assert(dest->getString("small", value) && value == "kept"); assert(!dest->containsKey("large")); dest->close(); } source->close(); printf("test import rejected writes: passed\n"); } void testImportRejectsSelf() { #ifndef MMKV_DISABLE_CRYPT const string key128 = "self-import-key"; const string key256 = "0123456789abcdef0123456789abcdef"; constexpr int cipherModes = 3; #else constexpr int cipherModes = 1; #endif const string medium(100, 'm'), large(300, 'l'); for (int cipher = 0; cipher < cipherModes; cipher++) { for (bool expire : {false, true}) { MMKVConfig config; #ifndef MMKV_DISABLE_CRYPT config.cryptKey = cipher == 0 ? nullptr : (cipher == 1 ? &key128 : &key256); config.aes256 = cipher == 2; #endif config.enableKeyExpire = expire; config.expiredInSeconds = 3600; auto kv = MMKV::mmkvWithID("import-self-" + to_string(cipher) + (expire ? "-expiring" : "-plain"), config); kv->clearAll(); assert(kv->set("short", "s")); assert(kv->set(medium, "m")); assert(kv->set(large, "l")); const auto sizeBefore = kv->actualSize(); for (bool reload : {false, true}) { if (reload) { kv->clearMemoryCache(); } assert(kv->importFrom(kv) == 0); assert(kv->actualSize() == sizeBefore); assert(kv->count() == 3); string value; assert(kv->getString("s", value) && value == "short"); assert(kv->getString("m", value) && value == medium); assert(kv->getString("l", value) && value == large); } kv->close(); } } printf("test import rejects self: passed\n"); } #ifndef MMKV_WIN32 void testImportUsesStableLockOrder() { auto first = MMKV::mmkvWithID("import-lock-order-a"); auto second = MMKV::mmkvWithID("import-lock-order-b"); first->clearAll(); second->clearAll(); assert(first->set("value", "key")); // Hold the lexicographically later store. A reverse-direction import must // lock the earlier store first, then wait for this lock. second->lock_thread(); atomic started{false}; thread worker([&] { started.store(true, memory_order_release); assert(second->importFrom(first) == 1); }); while (!started.load(memory_order_acquire)) { this_thread::yield(); } bool firstWasLocked = false; for (size_t attempt = 0; attempt < 5000; attempt++) { if (!first->try_lock_thread()) { firstWasLocked = true; break; } first->unlock_thread(); this_thread::sleep_for(chrono::milliseconds(1)); } second->unlock_thread(); worker.join(); assert(firstWasLocked); string value; assert(second->getString("key", value) && value == "value"); first->close(); second->close(); printf("test import stable lock order: passed\n"); } #endif #ifndef MMKV_DISABLE_CRYPT void testBorrowedValueStorage() { const auto inlineSize = KeyValueHolderCrypt::SmallBufferSize(); for (size_t size : {size_t(1), inlineSize, inlineSize + 1}) { vector source(size, 0x5a); MMBuffer borrowed(source.data(), source.size(), MMBufferNoCopy); KeyValueHolderCrypt holder(std::move(borrowed)); // Borrowed values that fit must not need a heap allocation. assert(holder.type == (size <= inlineSize ? KeyValueHolderType_Direct : KeyValueHolderType_Memory)); auto stored = holder.toMMBuffer(nullptr, nullptr); assert(stored.length() == source.size()); assert(stored.getPtr() != source.data()); fill(source.begin(), source.end(), 0); const auto bytes = static_cast(stored.getPtr()); assert(all_of(bytes, bytes + stored.length(), [](uint8_t value) { return value == 0x5a; })); } // An owned heap buffer must still transfer its allocation without copying. MMBuffer owned(inlineSize + 1); const auto ptr = owned.getPtr(); KeyValueHolderCrypt holder(std::move(owned)); assert(holder.type == KeyValueHolderType_Memory); assert(holder.memPtr == ptr); assert(owned.length() == 0); printf("test borrowed value storage: passed\n"); } // importFrom() hands the destination no-copy views of the source's values; // an encrypted destination must copy them, not take ownership of them. void testImportIntoEncrypted() { const string sourceKey = "import-source-key"; const string destKey = "import-dest-key"; const string destKey256 = "import-dest-key-32-bytes-long-ab"; const string medium(100, 'm'), large(300, 'l'); for (bool encryptedSource : {false, true}) { for (bool aes256 : {false, true}) { const string suffix = string(encryptedSource ? "-crypt" : "-plain") + (aes256 ? "-256" : "-128"); MMKVConfig sourceConfig; sourceConfig.cryptKey = encryptedSource ? &sourceKey : nullptr; auto source = MMKV::mmkvWithID("import-encrypted-source" + suffix, sourceConfig); source->clearAll(); assert(source->set("short", "s")); assert(source->set(medium, "m")); assert(source->set(medium, "m-retained")); assert(source->set(large, "l")); assert(source->set(int32_t(-42), "i")); assert(source->set(true, "b")); MMKVConfig destConfig; destConfig.cryptKey = aes256 ? &destKey256 : &destKey; destConfig.aes256 = aes256; auto dest = MMKV::mmkvWithID("import-encrypted-dest" + suffix, destConfig); dest->clearAll(); assert(dest->importFrom(source) == 6); // Import again over existing inline and heap-backed values. assert(dest->set("outdated", "s")); assert(dest->set(string(100, 'x'), "m-retained")); assert(dest->importFrom(source) == 6); string value; assert(dest->getString("s", value) && value == "short"); // overwriting and removing release what the destination holds assert(dest->set("changed", "s")); assert(dest->removeValueForKey("m")); // the source releases its own values source->clearAll(); source->close(); assert(dest->getString("s", value) && value == "changed"); assert(!dest->containsKey("m")); assert(dest->getString("m-retained", value) && value == medium); assert(dest->getString("l", value) && value == large); assert(dest->getInt32("i") == -42); assert(dest->getBool("b")); dest->trim(); dest->clearMemoryCache(); assert(dest->getString("m-retained", value) && value == medium); assert(dest->getString("l", value) && value == large); assert(dest->getInt32("i") == -42); assert(dest->getBool("b")); dest->close(); } } printf("test import into encrypted: passed\n"); } #endif void testFirstReadAfterReopenRespectsExpiration() { const string id = "first-read-expiration"; auto kv = MMKV::mmkvWithID(id); kv->clearAll(); assert(kv->enableAutoKeyExpire()); assert(kv->set(uint32_t(123), "expired", 1)); kv->sync(MMKV_SYNC); kv->close(); sleep(2); kv = MMKV::mmkvWithID(id); // Do not warm the lazy instance with count(), containsKey(), etc. // Before the fix, this FIRST getter returned 123; only the second getter missed. bool found = true; const auto value = kv->getUInt32("expired", 0, &found); assert(value == 0 && !found); kv->close(); printf("test first read after reopen respects expiration: passed\n"); } void testReadOnlyExpiration() { for (bool encrypted : {false, true}) { #ifdef MMKV_DISABLE_CRYPT if (encrypted) { continue; } #endif const string cryptKey = "expiration-key"; const string id = encrypted ? "readonly-expiration-crypt" : "readonly-expiration-plain"; auto kv = MMKV::mmkvWithID(id, MMKV_SINGLE_PROCESS, encrypted ? &cryptKey : nullptr); kv->clearAll(); assert(kv->enableAutoKeyExpire()); assert(kv->set(uint32_t(123), "expired", 1)); assert(kv->set("live", "live")); kv->sync(MMKV_SYNC); kv->close(); sleep(2); const auto mode = MMKV_SINGLE_PROCESS | MMKV_READ_ONLY; // Each API is the FIRST operation on its own read-only instance. kv = MMKV::mmkvWithID(id, mode, encrypted ? &cryptKey : nullptr); bool found = true; const auto value = kv->getUInt32("expired", 0, &found); assert(value == 0 && !found); // Previously returned 123: the metadata flag was ignored. kv->close(); kv = MMKV::mmkvWithID(id, mode, encrypted ? &cryptKey : nullptr); const auto keys = kv->allKeys(true); assert(keys == vector{"live"}); // Filtering must not require a writeback. kv->close(); kv = MMKV::mmkvWithID(id, mode, encrypted ? &cryptKey : nullptr); const auto count = kv->count(true); assert(count == 1); kv->close(); kv = MMKV::mmkvWithID(id, mode, encrypted ? &cryptKey : nullptr); assert(kv->count(false) == 2); // Read-only filtering must leave the stored entries intact. string live; assert(kv->getString("live", live) && live == "live"); kv->close(); } printf("test read-only expiration: passed\n"); } void testFirstWriteAfterReopenPreservesValue() { const string id = "first-write-expiration"; auto kv = MMKV::mmkvWithID(id); kv->clearAll(); assert(kv->enableAutoKeyExpire()); assert(kv->set("persisted", "existing")); kv->sync(MMKV_SYNC); kv->close(); kv = MMKV::mmkvWithID(id); // This set MUST be the first operation: metadata has not been lazily loaded yet. // Even ExpireNever needs a four-byte suffix when the store has expiration enabled. const auto written = kv->set("ABCD", "new"); assert(written); // Before the fix, set succeeded but omitted that suffix. string value; const auto found = kv->getString("new", value); assert(found && value == "ABCD"); // The payload must not be mistaken for a timestamp. kv->close(); printf("test first write after reopen preserves value: passed\n"); } // Coverage for the separate scalar, buffer, and vector encoding paths. // The minimal reproducer above explains the failure; this sweep guards each overload. void testFirstWriteAfterReopenOverloads() { const string id = "first-write-expiration-overloads"; auto kv = MMKV::mmkvWithID(id); kv->clearAll(); assert(kv->enableAutoKeyExpire()); auto reopen = [&] { kv->close(); kv = MMKV::mmkvWithID(id); }; // No getter or other warm-up is allowed between reopen() and each set(). reopen(); assert(kv->set(true, "bool", 60) && kv->getBool("bool")); reopen(); assert(kv->set(int32_t(-123), "i32", 60) && kv->getInt32("i32") == -123); reopen(); assert(kv->set(uint32_t(123), "u32", 60) && kv->getUInt32("u32") == 123); reopen(); assert(kv->set(int64_t(-123), "i64", 60) && kv->getInt64("i64") == -123); reopen(); assert(kv->set(uint64_t(123), "u64", 60) && kv->getUInt64("u64") == 123); reopen(); assert(kv->set(1.5f, "float", 60) && kv->getFloat("float") == 1.5f); reopen(); assert(kv->set(1.5, "double", 60) && kv->getDouble("double") == 1.5); string result; reopen(); assert(kv->set(string("ABCD"), "string", 60) && kv->getString("string", result) && result == "ABCD"); reopen(); assert(kv->set(string_view("ABCD"), "view", 60) && kv->getString("view", result) && result == "ABCD"); reopen(); char byteValue[] = "ABCD"; MMBuffer bytes(byteValue, 4, MMBufferNoCopy); assert(kv->set(bytes, "bytes", 60) && kv->getBytes("bytes") == bytes); reopen(); const vector strings = {"ABCD", "EFGH"}; vector stringsResult; assert(kv->set(strings, "strings", 60) && kv->getVector("strings", stringsResult) && stringsResult == strings); #ifdef MMKV_HAS_CPP20 reopen(); const vector ints = {1, 2, 3}; vector intsResult; assert(kv->set(ints, "ints", 60) && kv->getVector("ints", intsResult) && intsResult == ints); #endif kv->close(); printf("test first write after reopen overloads: passed\n"); } void testRemove(MMKV *mmkv) { auto ret = mmkv->set(true, "bool_1"); ret &= mmkv->set(numeric_limits::max(), "int_1"); ret &= mmkv->set(numeric_limits::max(), "long_1"); ret &= mmkv->set(numeric_limits::min(), "float_1"); ret &= mmkv->set(numeric_limits::min(), "double_1"); ret &= mmkv->set("hello", "string_1"); vector v = vector{"key", "value"}; ret &= mmkv->set(v, "vector_1"); assert(ret); { long count = mmkv->count(); mmkv->removeValueForKey("bool_1"); mmkv->removeValuesForKeys({"int_1", "long_1"}); auto newCount = mmkv->count(); assert(count == newCount + 3); } auto bValue = mmkv->getBool("bool_1"); assert(!bValue); auto iValue = mmkv->getInt32("int_1"); assert(iValue == 0); auto lValue = mmkv->getInt64("long_1"); assert(lValue == 0); auto fValue = mmkv->getFloat("float_1"); assert(EqualWithAccuracy(fValue, numeric_limits::min(), 0.001f)); double dValue = mmkv->getDouble("double_1"); assert(EqualWithAccuracy(dValue, numeric_limits::min(), 0.001)); string sValue; ret = mmkv->getString("string_1", sValue); assert(ret && sValue == "hello"); vector vValue; ret = mmkv->getVector("vector_1", vValue); assert(ret && vValue == v); printf("test remove: passed\n"); } int main(int argc, char *argv[]) { locale::global(locale("")); wcout.imbue(locale("")); char c; srand((uint64_t) &c); string rootDir = argc > 1 ? argv[1] : "/tmp/mmkv"; assert(mkPath(rootDir)); MMKV::initializeMMKV(rootDir); auto mmkv = MMKV::mmkvWithID("unit_test"); mmkv->clearAll(); testBool(mmkv); testInt32(mmkv); testInt64(mmkv); testUInt32(mmkv); testUInt64(mmkv); testFloat(mmkv); testDouble(mmkv); testString(mmkv); testBytes(mmkv); testVector(mmkv); testRemove(mmkv); testExpirationMigrationAggregateLimit(); testOversizedKey(mmkv); testOversizedValue(mmkv); testFileValidation(rootDir); testTinyFileRecovery(rootDir); testCodedOutputBounds(); testExpirationOverflow(); testExpirationAlignment(); testArmCRC32(); #ifndef MMKV_DISABLE_CRYPT testCryptoRandomAndWipe(); testDecryptToPlainKeyLimit(); #endif testLongDirectoryWalk(rootDir); testMinimalBackupRestore(rootDir); testCachedRestore(rootDir); testReadOnlyRecovery(rootDir); testImportRejectedWrites(); testImportRejectsSelf(); #ifndef MMKV_WIN32 testImportUsesStableLockOrder(); #endif #ifndef MMKV_DISABLE_CRYPT testBorrowedValueStorage(); testImportIntoEncrypted(); #endif testFirstReadAfterReopenRespectsExpiration(); testReadOnlyExpiration(); testFirstWriteAfterReopenPreservesValue(); testFirstWriteAfterReopenOverloads(); }