Files

1357 lines
48 KiB
C++

/*
* 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 <MMKV/MMKV.h>
#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 <algorithm>
#include <array>
#include <atomic>
#include <cassert>
#include <chrono>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <fcntl.h>
#include <iostream>
#include <limits.h>
#include <limits>
#include <numeric>
#include <new>
#include <sys/stat.h>
#include <thread>
#include <unistd.h>
#include <utility>
#include <vector>
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<int32_t>::max(), "int32");
assert(ret);
auto value = mmkv->getInt32("int32");
assert(value == numeric_limits<int32_t>::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<uint32_t>::max(), "uint32");
assert(ret);
auto value = mmkv->getUInt32("uint32");
assert(value == numeric_limits<uint32_t>::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<int64_t>::min(), "int64");
assert(ret);
auto value = mmkv->getInt64("int64");
assert(value == numeric_limits<int64_t>::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<uint64_t>::max(), "uint64");
assert(ret);
auto value = mmkv->getUInt64("uint64");
assert(value == numeric_limits<uint64_t>::max());
value = mmkv->getUInt64(KeyNotExist);
assert(value == 0);
value = mmkv->getUInt64(KeyNotExist, -1);
assert(value == -1);
printf("test uint64: passed\n");
}
template <typename T>
bool EqualWithAccuracy(T value1, T value2, T accuracy) {
return fabs(value1 - value2) <= accuracy;
}
void testFloat(MMKV *mmkv) {
auto ret = mmkv->set(numeric_limits<float>::max(), "float");
assert(ret);
auto value = mmkv->getFloat("float");
assert(EqualWithAccuracy(value, numeric_limits<float>::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<double>::max(), "double");
assert(ret);
auto value = mmkv->getDouble("double");
assert(EqualWithAccuracy(value, numeric_limits<double>::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<string> v = {"1", "0", "2", "4"};
auto ret = mmkv->set(v, "vector");
assert(ret);
vector<string> 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 <MMKVMap *MMKV::*Member> 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<uint32_t>::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<size_t>(numeric_limits<uint32_t>::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<size_t>::max() > numeric_limits<uint32_t>::max()) {
MMBuffer oversized(&sentinel, static_cast<size_t>(numeric_limits<uint32_t>::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<size_t>::max() > numeric_limits<uint32_t>::max()) {
MMBuffer oversized(&sentinel, static_cast<size_t>(numeric_limits<uint32_t>::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<off_t>(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<uint32_t>::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<uint32_t>::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<uint32_t>::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<uint8_t, 16> 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<uint32_t, 6> values32 = {0, 127, 128, 16383, 16384, numeric_limits<uint32_t>::max()};
const vector<vector<uint8_t>> 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<uint8_t, 5> 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<uint64_t, 6> values64 = {0, 127, 128, 16383, 16384, numeric_limits<uint64_t>::max()};
const vector<vector<uint8_t>> 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<uint8_t, 4> 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<size_t>::max() > numeric_limits<int32_t>::max()) {
storage.fill(0xA5);
MMBuffer wideValue(storage.data(), static_cast<size_t>(numeric_limits<int32_t>::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<size_t>::max() > numeric_limits<uint32_t>::max()) {
storage.fill(0xA5);
MMBuffer oversized(storage.data(), static_cast<size_t>(numeric_limits<uint32_t>::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<uint8_t, 265> storage = {};
for (size_t index = 0; index < storage.size(); index++) {
storage[index] = static_cast<uint8_t>(index * 37 + 11);
}
constexpr array<uint32_t, 3> 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<char> mutableTemplate(baseTemplate.begin(), baseTemplate.end());
mutableTemplate.push_back('\0');
auto base = mkdtemp(mutableTemplate.data());
assert(base);
string path(base);
vector<pair<int, string>> 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<size_t>(100, remainingLength - 1);
string name(nameLength, static_cast<char>('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<string> 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<unsigned long long>(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<ssize_t>(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<uint32_t>(kv->actualSize());
auto badSize = static_cast<uint32_t>(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<bool> 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<uint8_t> 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<const uint8_t *>(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<string>{"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<string> strings = {"ABCD", "EFGH"};
vector<string> stringsResult;
assert(kv->set(strings, "strings", 60) && kv->getVector("strings", stringsResult) && stringsResult == strings);
#ifdef MMKV_HAS_CPP20
reopen();
const vector<int32_t> ints = {1, 2, 3};
vector<int32_t> 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<int32_t>::max(), "int_1");
ret &= mmkv->set(numeric_limits<int64_t>::max(), "long_1");
ret &= mmkv->set(numeric_limits<float>::min(), "float_1");
ret &= mmkv->set(numeric_limits<double>::min(), "double_1");
ret &= mmkv->set("hello", "string_1");
vector<string> v = vector<string>{"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<float>::min(), 0.001f));
double dValue = mmkv->getDouble("double_1");
assert(EqualWithAccuracy(dValue, numeric_limits<double>::min(), 0.001));
string sValue;
ret = mmkv->getString("string_1", sValue);
assert(ret && sValue == "hello");
vector<string> 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();
}