subrepo:
  subdir:   "libraries"
  merged:   "07af583b"
upstream:
  origin:   "https://github.com/Atmosphere-NX/Atmosphere-libs"
  branch:   "master"
  commit:   "07af583b"
git-subrepo:
  version:  "0.4.0"
  origin:   "https://github.com/ingydotnet/git-subrepo"
  commit:   "5d6aba9"
This commit is contained in:
Michael Scire
2019-12-09 03:57:37 -08:00
committed by SciresM
parent 28717bfd27
commit 0105455086
294 changed files with 29915 additions and 0 deletions

View File

@@ -0,0 +1,64 @@
/*
* Copyright (c) 2018-2019 Atmosphère-NX
*
* This program is free software; you can redistribute it and/or modify it
* under the terms and conditions of the GNU General Public License,
* version 2, as published by the Free Software Foundation.
*
* This program is distributed in the hope it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "kvdb_auto_buffer.hpp"
namespace ams::kvdb {
/* Functionality for parsing/generating a key value archive. */
class ArchiveReader {
private:
AutoBuffer &buffer;
size_t offset;
public:
ArchiveReader(AutoBuffer &b) : buffer(b), offset(0) { /* ... */ }
private:
Result Peek(void *dst, size_t size);
Result Read(void *dst, size_t size);
public:
Result ReadEntryCount(size_t *out);
Result GetEntrySize(size_t *out_key_size, size_t *out_value_size);
Result ReadEntry(void *out_key, size_t key_size, void *out_value, size_t value_size);
};
class ArchiveWriter {
private:
AutoBuffer &buffer;
size_t offset;
public:
ArchiveWriter(AutoBuffer &b) : buffer(b), offset(0) { /* ... */ }
private:
Result Write(const void *src, size_t size);
public:
void WriteHeader(size_t entry_count);
void WriteEntry(const void *key, size_t key_size, const void *value, size_t value_size);
};
class ArchiveSizeHelper {
private:
size_t size;
public:
ArchiveSizeHelper();
void AddEntry(size_t key_size, size_t value_size);
size_t GetSize() const {
return this->size;
}
};
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright (c) 2018-2019 Atmosphère-NX
*
* This program is free software; you can redistribute it and/or modify it
* under the terms and conditions of the GNU General Public License,
* version 2, as published by the Free Software Foundation.
*
* This program is distributed in the hope it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <vapours.hpp>
namespace ams::kvdb {
class AutoBuffer {
NON_COPYABLE(AutoBuffer);
private:
u8 *buffer;
size_t size;
public:
AutoBuffer() : buffer(nullptr), size(0) { /* ... */ }
~AutoBuffer() {
this->Reset();
}
AutoBuffer(AutoBuffer &&rhs) {
this->buffer = rhs.buffer;
this->size = rhs.size;
rhs.buffer = nullptr;
rhs.size = 0;
}
AutoBuffer& operator=(AutoBuffer &&rhs) {
rhs.Swap(*this);
return *this;
}
void Swap(AutoBuffer &rhs) {
std::swap(this->buffer, rhs.buffer);
std::swap(this->size, rhs.size);
}
void Reset() {
if (this->buffer != nullptr) {
std::free(this->buffer);
this->buffer = nullptr;
this->size = 0;
}
}
u8 *Get() const {
return this->buffer;
}
size_t GetSize() const {
return this->size;
}
Result Initialize(size_t size) {
/* Check that we're not already initialized. */
AMS_ASSERT(this->buffer == nullptr);
/* Allocate a buffer. */
this->buffer = static_cast<u8 *>(std::malloc(size));
if (this->buffer == nullptr) {
return ResultAllocationFailed();
}
this->size = size;
return ResultSuccess();
}
Result Initialize(const void *buf, size_t size) {
/* Create a new buffer of the right size. */
R_TRY(this->Initialize(size));
/* Copy the input data in. */
std::memcpy(this->buffer, buf, size);
return ResultSuccess();
}
};
}

View File

@@ -0,0 +1,145 @@
/*
* Copyright (c) 2018-2019 Atmosphère-NX
*
* This program is free software; you can redistribute it and/or modify it
* under the terms and conditions of the GNU General Public License,
* version 2, as published by the Free Software Foundation.
*
* This program is distributed in the hope it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <vapours.hpp>
namespace ams::kvdb {
/* Represents a string with a backing buffer of N bytes. */
template<size_t N>
class BoundedString {
static_assert(N > 0, "BoundedString requires non-zero backing buffer!");
private:
char buffer[N];
private:
/* Utility. */
static inline void CheckLength(size_t len) {
AMS_ASSERT(len < N);
}
public:
/* Constructors. */
constexpr BoundedString() {
buffer[0] = 0;
}
explicit constexpr BoundedString(const char *s) {
this->Set(s);
}
/* Static constructors. */
static constexpr BoundedString<N> Make(const char *s) {
return BoundedString<N>(s);
}
static constexpr BoundedString<N> MakeFormat(const char *format, ...) __attribute__((format (printf, 1, 2))) {
BoundedString<N> string;
std::va_list args;
va_start(args, format);
CheckLength(std::vsnprintf(string.buffer, N, format, args));
va_end(args);
return string;
}
/* Getters. */
size_t GetLength() const {
return strnlen(this->buffer, N);
}
const char *Get() const {
return this->buffer;
}
operator const char *() const {
return this->buffer;
}
/* Setters. */
void Set(const char *s) {
/* Ensure string can fit in our buffer. */
CheckLength(strnlen(s, N));
std::strncpy(this->buffer, s, N);
}
void SetFormat(const char *format, ...) __attribute__((format (printf, 2, 3))) {
/* Format into the buffer, abort if too large. */
std::va_list args;
va_start(args, format);
CheckLength(std::vsnprintf(this->buffer, N, format, args));
va_end(args);
}
/* Append to existing. */
void Append(const char *s) {
const size_t length = GetLength();
CheckLength(length + strnlen(s, N));
std::strncat(this->buffer, s, N - length - 1);
}
void Append(char c) {
const size_t length = GetLength();
CheckLength(length + 1);
this->buffer[length] = c;
this->buffer[length + 1] = 0;
}
void AppendFormat(const char *format, ...) __attribute__((format (printf, 2, 3))) {
const size_t length = GetLength();
std::va_list args;
va_start(args, format);
CheckLength(std::vsnprintf(this->buffer + length, N - length, format, args) + length);
va_end(args);
}
/* Substring utilities. */
void GetSubstring(char *dst, size_t dst_size, size_t offset, size_t length) const {
/* Make sure output buffer can hold the substring. */
AMS_ASSERT(offset + length <= GetLength());
AMS_ASSERT(dst_size > length);
/* Copy substring to dst. */
std::strncpy(dst, this->buffer + offset, length);
dst[length] = 0;
}
BoundedString<N> GetSubstring(size_t offset, size_t length) const {
BoundedString<N> string;
GetSubstring(string.buffer, N, offset, length);
return string;
}
/* Comparison. */
constexpr bool operator==(const BoundedString<N> &rhs) const {
return std::strncmp(this->buffer, rhs.buffer, N) == 0;
}
constexpr bool operator!=(const BoundedString<N> &rhs) const {
return !(*this == rhs);
}
bool EndsWith(const char *s, size_t offset) const {
return std::strncmp(this->buffer + offset, s, N - offset) == 0;
}
bool EndsWith(const char *s) const {
const size_t suffix_length = strnlen(s, N);
const size_t length = GetLength();
return suffix_length <= length && EndsWith(s, length - suffix_length);
}
};
}

View File

@@ -0,0 +1,391 @@
/*
* Copyright (c) 2018-2019 Atmosphère-NX
*
* This program is free software; you can redistribute it and/or modify it
* under the terms and conditions of the GNU General Public License,
* version 2, as published by the Free Software Foundation.
*
* This program is distributed in the hope it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <sys/stat.h>
#include "kvdb_bounded_string.hpp"
#include "kvdb_file_key_value_store.hpp"
namespace ams::kvdb {
namespace impl {
template<class Key, size_t Capacity>
class LruList {
private:
/* Subtypes. */
struct LruHeader {
u32 entry_count;
};
public:
static constexpr size_t BufferSize = sizeof(Key) * Capacity;
static constexpr size_t FileSize = sizeof(LruHeader) + BufferSize;
using Path = FileKeyValueStore::Path;
private:
Path file_path;
Key *keys;
LruHeader header;
public:
static Result CreateNewList(const char *path) {
/* Create new lru_list.dat. */
R_TRY(fsdevCreateFile(path, FileSize, 0));
/* Open the file. */
FILE *fp = fopen(path, "r+b");
R_UNLESS(fp != nullptr, fsdevGetLastResult());
ON_SCOPE_EXIT { fclose(fp); };
/* Write new header with zero entries to the file. */
LruHeader new_header = { .entry_count = 0, };
R_UNLESS(fwrite(&new_header, sizeof(new_header), 1, fp) == 1, fsdevGetLastResult());
return ResultSuccess();
}
private:
void RemoveIndex(size_t i) {
AMS_ASSERT(i < this->GetCount());
std::memmove(this->keys + i, this->keys + i + 1, sizeof(*this->keys) * (this->GetCount() - (i + 1)));
this->DecrementCount();
}
void IncrementCount() {
this->header.entry_count++;
}
void DecrementCount() {
this->header.entry_count--;
}
public:
LruList() : keys(nullptr), header({}) { /* ... */ }
Result Initialize(const char *path, void *buf, size_t size) {
/* Only initialize once, and ensure we have sufficient memory. */
AMS_ASSERT(this->keys == nullptr);
AMS_ASSERT(size >= BufferSize);
/* Setup member variables. */
this->keys = static_cast<Key *>(buf);
this->file_path.Set(path);
std::memset(this->keys, 0, BufferSize);
/* Open file. */
FILE *fp = fopen(this->file_path, "rb");
R_UNLESS(fp != nullptr, fsdevGetLastResult());
ON_SCOPE_EXIT { fclose(fp); };
/* Read header. */
R_UNLESS(fread(&this->header, sizeof(this->header), 1, fp) == 1, fsdevGetLastResult());
/* Read entries. */
const size_t count = this->GetCount();
if (count > 0) {
R_UNLESS(fread(this->keys, std::min(BufferSize, sizeof(Key) * count), 1, fp) == 1, fsdevGetLastResult());
}
return ResultSuccess();
}
Result Save() {
/* Open file. */
FILE *fp = fopen(this->file_path, "r+b");
R_UNLESS(fp != nullptr, fsdevGetLastResult());
ON_SCOPE_EXIT { fclose(fp); };
/* Write header. */
R_UNLESS(fwrite(&this->header, sizeof(this->header), 1, fp) == 1, fsdevGetLastResult());
/* Write entries. */
R_UNLESS(fwrite(this->keys, BufferSize, 1, fp) == 1, fsdevGetLastResult());
/* Flush. */
fflush(fp);
return ResultSuccess();
}
size_t GetCount() const {
return this->header.entry_count;
}
bool IsEmpty() const {
return this->GetCount() == 0;
}
bool IsFull() const {
return this->GetCount() >= Capacity;
}
Key Get(size_t i) const {
AMS_ASSERT(i < this->GetCount());
return this->keys[i];
}
Key Peek() const {
AMS_ASSERT(!this->IsEmpty());
return this->Get(0);
}
void Push(const Key &key) {
AMS_ASSERT(!this->IsFull());
this->keys[this->GetCount()] = key;
this->IncrementCount();
}
Key Pop() {
AMS_ASSERT(!this->IsEmpty());
this->RemoveIndex(0);
}
bool Remove(const Key &key) {
const size_t count = this->GetCount();
/* Iterate over the list, removing the last entry that matches the key. */
for (size_t i = 0; i < count; i++) {
if (this->keys[count - 1 - i] == key) {
this->RemoveIndex(count - 1 - i);
return true;
}
}
return false;
}
bool Contains(const Key &key) const {
const size_t count = this->GetCount();
/* Iterate over the list, checking to see if we have the key. */
for (size_t i = 0; i < count; i++) {
if (this->keys[count - 1 - i] == key) {
return true;
}
}
return false;
}
bool Update(const Key &key) {
if (this->Remove(key)) {
this->Push(key);
return true;
}
return false;
}
};
}
template<class Key, size_t Capacity>
class FileKeyValueCache {
static_assert(std::is_pod<Key>::value, "FileKeyValueCache Key must be pod!");
static_assert(sizeof(Key) <= FileKeyValueStore::MaxKeySize, "FileKeyValueCache Key is too big!");
public:
using LeastRecentlyUsedList = impl::LruList<Key, Capacity>;
/* Note: Nintendo code in NS uses Path = BoundedString<0x180> here. */
/* It's unclear why, since they use 0x300 everywhere else. */
/* We'll just use 0x300, since it shouldn't make a difference, */
/* as FileKeyValueStore paths are limited to 0x100 anyway. */
using Path = typename LeastRecentlyUsedList::Path;
private:
FileKeyValueStore kvs;
LeastRecentlyUsedList lru_list;
private:
static constexpr Path GetLeastRecentlyUsedListPath(const char *dir) {
return Path::MakeFormat("%s/%s", dir, "lru_list.dat");
}
static constexpr Path GetFileKeyValueStorePath(const char *dir) {
return Path::MakeFormat("%s/%s", dir, "kvs");
}
static Result Exists(bool *out, const char *path, bool is_dir) {
/* Set out to false initially. */
*out = false;
/* Check that the path exists, and that our entry type is correct. */
{
struct stat st;
if (stat(path, &st) != 0) {
R_TRY_CATCH(fsdevGetLastResult()) {
/* If the path doesn't exist, nothing has gone wrong. */
R_CONVERT(fs::ResultPathNotFound, ResultSuccess());
} R_END_TRY_CATCH;
}
if (is_dir) {
R_UNLESS((S_ISDIR(st.st_mode)), ResultInvalidFilesystemState());
} else {
R_UNLESS((S_ISREG(st.st_mode)), ResultInvalidFilesystemState());
}
}
*out = true;
return ResultSuccess();
}
static Result DirectoryExists(bool *out, const char *path) {
return Exists(out, path, true);
}
static Result FileExists(bool *out, const char *path) {
return Exists(out, path, false);
}
public:
static Result CreateNewCache(const char *dir) {
/* Make a new key value store filesystem, and a new lru_list.dat. */
R_TRY(LeastRecentlyUsedList::CreateNewList(GetLeastRecentlyUsedListPath(dir)));
R_UNLESS(mkdir(GetFileKeyValueStorePath(dir), 0) == 0, fsdevGetLastResult());
return ResultSuccess();
}
static Result ValidateExistingCache(const char *dir) {
/* Check for existence. */
bool has_lru = false, has_kvs = false;
R_TRY(FileExists(&has_lru, GetLeastRecentlyUsedListPath(dir)));
R_TRY(DirectoryExists(&has_kvs, GetFileKeyValueStorePath(dir)));
/* If neither exists, CreateNewCache was never called. */
R_UNLESS(has_lru || has_kvs, ResultNotCreated());
/* If one exists but not the other, we have an invalid state. */
R_UNLESS(has_lru && has_kvs, ResultInvalidFilesystemState());
return ResultSuccess();
}
private:
void RemoveOldestKey() {
const Key &oldest_key = this->lru_list.Peek();
this->lru_list.Pop();
this->kvs.Remove(oldest_key);
}
public:
Result Initialize(const char *dir, void *buf, size_t size) {
/* Initialize list. */
R_TRY(this->lru_list.Initialize(GetLeastRecentlyUsedListPath(dir), buf, size));
/* Initialize kvs. */
/* NOTE: Despite creating the kvs folder and returning an error if it does not exist, */
/* Nintendo does not use the kvs folder, and instead uses the passed dir. */
/* This causes lru_list.dat to be in the same directory as the store's .val files */
/* instead of in the same directory as a folder containing the store's .val files. */
/* This is probably a Nintendo bug, but because system saves contain data in the wrong */
/* layout it can't really be fixed without breaking existing devices... */
R_TRY(this->kvs.Initialize(dir));
return ResultSuccess();
}
size_t GetCount() const {
return this->lru_list.GetCount();
}
size_t GetCapacity() const {
return Capacity;
}
Key GetKey(size_t i) const {
return this->lru_list.Get(i);
}
bool Contains(const Key &key) const {
return this->lru_list.Contains(key);
}
Result Get(size_t *out_size, void *out_value, size_t max_out_size, const Key &key) {
/* Note that we accessed the key. */
this->lru_list.Update(key);
return this->kvs.Get(out_size, out_value, max_out_size, key);
}
template<typename Value>
Result Get(Value *out_value, const Key &key) {
/* Note that we accessed the key. */
this->lru_list.Update(key);
return this->kvs.Get(out_value, key);
}
Result GetSize(size_t *out_size, const Key &key) {
return this->kvs.GetSize(out_size, key);
}
Result Set(const Key &key, const void *value, size_t value_size) {
if (this->lru_list.Update(key)) {
/* If an entry for the key exists, delete the existing value file. */
this->kvs.Remove(key);
} else {
/* If the list is full, we need to remove the oldest key. */
if (this->lru_list.IsFull()) {
this->RemoveOldestKey();
}
/* Add the key to the list. */
this->lru_list.Push(key);
}
/* Loop, trying to save the new value to disk. */
while (true) {
/* Try to set the key. */
R_TRY_CATCH(this->kvs.Set(key, value, value_size)) {
R_CATCH(fs::ResultNotEnoughFreeSpace) {
/* If our entry is the only thing in the Lru list, remove it. */
if (this->lru_list.GetCount() == 1) {
this->lru_list.Pop();
R_TRY(this->lru_list.Save());
return fs::ResultNotEnoughFreeSpace();
}
/* Otherwise, remove the oldest element from the cache and try again. */
this->RemoveOldestKey();
continue;
}
} R_END_TRY_CATCH;
/* If we got here, we succeeded. */
break;
}
/* Save the list. */
R_TRY(this->lru_list.Save());
return ResultSuccess();
}
template<typename Value>
Result Set(const Key &key, const Value &value) {
return this->Set(key, &value, sizeof(Value));
}
Result Remove(const Key &key) {
/* Remove the key. */
this->lru_list.Remove(key);
R_TRY(this->kvs.Remove(key));
R_TRY(this->lru_list.Save());
return ResultSuccess();
}
Result RemoveAll() {
/* TODO: Nintendo doesn't check errors here. Should we? */
while (!this->lru_list.IsEmpty()) {
this->RemoveOldestKey();
}
R_TRY(this->lru_list.Save());
return ResultSuccess();
}
};
}

View File

@@ -0,0 +1,122 @@
/*
* Copyright (c) 2018-2019 Atmosphère-NX
*
* This program is free software; you can redistribute it and/or modify it
* under the terms and conditions of the GNU General Public License,
* version 2, as published by the Free Software Foundation.
*
* This program is distributed in the hope it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "../os.hpp"
#include "kvdb_bounded_string.hpp"
namespace ams::kvdb {
class FileKeyValueStore {
NON_COPYABLE(FileKeyValueStore);
NON_MOVEABLE(FileKeyValueStore);
public:
static constexpr size_t MaxPathLength = 0x300; /* TODO: FS_MAX_PATH - 1? */
static constexpr size_t MaxFileLength = 0xFF;
static constexpr char FileExtension[5] = ".val";
static constexpr size_t FileExtensionLength = sizeof(FileExtension) - 1;
static constexpr size_t MaxKeySize = (MaxFileLength - FileExtensionLength) / 2;
using Path = kvdb::BoundedString<MaxPathLength>;
using FileName = kvdb::BoundedString<MaxFileLength>;
private:
/* Subtypes. */
struct Entry {
u8 key[MaxKeySize];
void *value;
size_t key_size;
size_t value_size;
};
static_assert(std::is_pod<Entry>::value, "FileKeyValueStore::Entry definition!");
class Cache {
private:
u8 *backing_buffer = nullptr;
size_t backing_buffer_size = 0;
size_t backing_buffer_free_offset = 0;
Entry *entries = nullptr;
size_t count = 0;
size_t capacity = 0;
private:
void *Allocate(size_t size);
bool HasEntries() const {
return this->entries != nullptr && this->capacity != 0;
}
public:
Result Initialize(void *buffer, size_t buffer_size, size_t capacity);
void Invalidate();
std::optional<size_t> TryGet(void *out_value, size_t max_out_size, const void *key, size_t key_size);
std::optional<size_t> TryGetSize(const void *key, size_t key_size);
void Set(const void *key, size_t key_size, const void *value, size_t value_size);
bool Contains(const void *key, size_t key_size);
};
private:
os::Mutex lock;
Path dir_path;
Cache cache;
private:
Path GetPath(const void *key, size_t key_size);
Result GetKey(size_t *out_size, void *out_key, size_t max_out_size, const FileName &file_name);
public:
FileKeyValueStore() { /* ... */ }
/* Basic accessors. */
Result Initialize(const char *dir);
Result InitializeWithCache(const char *dir, void *cache_buffer, size_t cache_buffer_size, size_t cache_capacity);
Result Get(size_t *out_size, void *out_value, size_t max_out_size, const void *key, size_t key_size);
Result GetSize(size_t *out_size, const void *key, size_t key_size);
Result Set(const void *key, size_t key_size, const void *value, size_t value_size);
Result Remove(const void *key, size_t key_size);
/* Niceties. */
template<typename Key>
Result Get(size_t *out_size, void *out_value, size_t max_out_size, const Key &key) {
static_assert(std::is_pod<Key>::value && sizeof(Key) <= MaxKeySize, "Invalid FileKeyValueStore Key!");
return this->Get(out_size, out_value, max_out_size, &key, sizeof(Key));
}
template<typename Key, typename Value>
Result Get(Value *out_value, const Key &key) {
static_assert(std::is_pod<Value>::value && !std::is_pointer<Value>::value, "Invalid FileKeyValueStore Value!");
size_t size = 0;
R_TRY(this->Get(&size, out_value, sizeof(Value), key));
AMS_ASSERT(size >= sizeof(Value));
return ResultSuccess();
}
template<typename Key>
Result GetSize(size_t *out_size, const Key &key) {
return this->GetSize(out_size, &key, sizeof(Key));
}
template<typename Key>
Result Set(const Key &key, const void *value, size_t value_size) {
static_assert(std::is_pod<Key>::value && sizeof(Key) <= MaxKeySize, "Invalid FileKeyValueStore Key!");
return this->Set(&key, sizeof(Key), value, value_size);
}
template<typename Key, typename Value>
Result Set(const Key &key, const Value &value) {
static_assert(std::is_pod<Value>::value && !std::is_pointer<Value>::value, "Invalid FileKeyValueStore Value!");
return this->Set(key, &value, sizeof(Value));
}
template<typename Key>
Result Remove(const Key &key) {
return this->Remove(&key, sizeof(Key));
}
};
}

View File

@@ -0,0 +1,525 @@
/*
* Copyright (c) 2018-2019 Atmosphère-NX
*
* This program is free software; you can redistribute it and/or modify it
* under the terms and conditions of the GNU General Public License,
* version 2, as published by the Free Software Foundation.
*
* This program is distributed in the hope it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <sys/stat.h>
#include "kvdb_auto_buffer.hpp"
#include "kvdb_archive.hpp"
#include "kvdb_bounded_string.hpp"
namespace ams::kvdb {
template<class Key>
class MemoryKeyValueStore {
static_assert(std::is_pod<Key>::value, "KeyValueStore Keys must be pod!");
NON_COPYABLE(MemoryKeyValueStore);
NON_MOVEABLE(MemoryKeyValueStore);
public:
/* Subtypes. */
class Entry {
private:
Key key;
void *value;
size_t value_size;
public:
constexpr Entry(const Key &k, void *v, size_t s) : key(k), value(v), value_size(s) { /* ... */ }
const Key &GetKey() const {
return this->key;
}
template<typename Value = void>
Value *GetValuePointer() {
/* Size check. Note: Nintendo does not size check. */
if constexpr (!std::is_same<Value, void>::value) {
AMS_ASSERT(sizeof(Value) <= this->value_size);
/* Ensure we only get pod. */
static_assert(std::is_pod<Value>::value, "KeyValueStore Values must be pod");
}
return reinterpret_cast<Value *>(this->value);
}
template<typename Value = void>
const Value *GetValuePointer() const {
/* Size check. Note: Nintendo does not size check. */
if constexpr (!std::is_same<Value, void>::value) {
AMS_ASSERT(sizeof(Value) <= this->value_size);
/* Ensure we only get pod. */
static_assert(std::is_pod<Value>::value, "KeyValueStore Values must be pod");
}
return reinterpret_cast<Value *>(this->value);
}
template<typename Value>
Value &GetValue() {
return *(this->GetValuePointer<Value>());
}
template<typename Value>
const Value &GetValue() const {
return *(this->GetValuePointer<Value>());
}
size_t GetValueSize() const {
return this->value_size;
}
constexpr inline bool operator<(const Key &rhs) const {
return key < rhs;
}
constexpr inline bool operator==(const Key &rhs) const {
return key == rhs;
}
};
class Index {
private:
size_t count;
size_t capacity;
Entry *entries;
public:
Index() : count(0), capacity(0), entries(nullptr) { /* ... */ }
~Index() {
if (this->entries != nullptr) {
this->ResetEntries();
std::free(this->entries);
this->entries = nullptr;
}
}
size_t GetCount() const {
return this->count;
}
size_t GetCapacity() const {
return this->capacity;
}
void ResetEntries() {
for (size_t i = 0; i < this->count; i++) {
std::free(this->entries[i].GetValuePointer());
}
this->count = 0;
}
Result Initialize(size_t capacity) {
this->entries = reinterpret_cast<Entry *>(std::malloc(sizeof(Entry) * capacity));
R_UNLESS(this->entries != nullptr, ResultAllocationFailed());
this->capacity = capacity;
return ResultSuccess();
}
Result Set(const Key &key, const void *value, size_t value_size) {
/* Allocate new value. */
void *new_value = std::malloc(value_size);
R_UNLESS(new_value != nullptr, ResultAllocationFailed());
auto value_guard = SCOPE_GUARD { std::free(new_value); };
std::memcpy(new_value, value, value_size);
/* Find entry for key. */
Entry *it = this->lower_bound(key);
if (it != this->end() && it->GetKey() == key) {
/* Entry already exists. Free old value. */
std::free(it->GetValuePointer());
} else {
/* We need to add a new entry. Check we have room, move future keys forward. */
R_UNLESS(this->count < this->capacity, ResultOutOfKeyResource());
std::memmove(it + 1, it, sizeof(*it) * (this->end() - it));
this->count++;
}
/* Save the new Entry in the map. */
value_guard.Cancel();
*it = Entry(key, new_value, value_size);
return ResultSuccess();
}
Result AddUnsafe(const Key &key, void *value, size_t value_size) {
R_UNLESS(this->count < this->capacity, ResultOutOfKeyResource());
this->entries[this->count++] = Entry(key, value, value_size);
return ResultSuccess();
}
Result Remove(const Key &key) {
/* Find entry for key. */
Entry *it = this->find(key);
R_UNLESS(it != this->end(), ResultKeyNotFound());
/* Free the value, move entries back. */
std::free(it->GetValuePointer());
std::memmove(it, it + 1, sizeof(*it) * (this->end() - (it + 1)));
this->count--;
return ResultSuccess();
}
Entry *begin() {
return this->GetBegin();
}
const Entry *begin() const {
return this->GetBegin();
}
Entry *end() {
return this->GetEnd();
}
const Entry *end() const {
return this->GetEnd();
}
const Entry *cbegin() const {
return this->begin();
}
const Entry *cend() const {
return this->end();
}
Entry *lower_bound(const Key &key) {
return this->GetLowerBound(key);
}
const Entry *lower_bound(const Key &key) const {
return this->GetLowerBound(key);
}
Entry *find(const Key &key) {
return this->Find(key);
}
const Entry *find(const Key &key) const {
return this->Find(key);
}
private:
Entry *GetBegin() {
return this->entries;
}
const Entry *GetBegin() const {
return this->entries;
}
Entry *GetEnd() {
return this->GetBegin() + this->count;
}
const Entry *GetEnd() const {
return this->GetBegin() + this->count;
}
Entry *GetLowerBound(const Key &key) {
return std::lower_bound(this->GetBegin(), this->GetEnd(), key);
}
const Entry *GetLowerBound(const Key &key) const {
return std::lower_bound(this->GetBegin(), this->GetEnd(), key);
}
Entry *Find(const Key &key) {
auto it = this->GetLowerBound(key);
auto end = this->GetEnd();
if (it != end && it->GetKey() == key) {
return it;
}
return end;
}
const Entry *Find(const Key &key) const {
auto it = this->GetLowerBound(key);
auto end = this->GetEnd();
if (it != end && it->GetKey() == key) {
return it;
}
return end;
}
};
private:
static constexpr size_t MaxPathLen = 0x300; /* TODO: FS_MAX_PATH - 1? */
using Path = kvdb::BoundedString<MaxPathLen>;
private:
Index index;
Path path;
Path temp_path;
public:
MemoryKeyValueStore() { /* ... */ }
Result Initialize(const char *dir, size_t capacity) {
/* Ensure that the passed path is a directory. */
{
struct stat st;
R_UNLESS(stat(dir, &st) == 0, fs::ResultPathNotFound());
R_UNLESS((S_ISDIR(st.st_mode)), fs::ResultPathNotFound());
}
/* Set paths. */
this->path.SetFormat("%s%s", dir, "/imkvdb.arc");
this->temp_path.SetFormat("%s%s", dir, "/imkvdb.tmp");
/* Initialize our index. */
R_TRY(this->index.Initialize(capacity));
return ResultSuccess();
}
Result Initialize(size_t capacity) {
/* This initializes without an archive file. */
/* A store initialized this way cannot have its contents loaded from or flushed to disk. */
this->path.Set("");
this->temp_path.Set("");
/* Initialize our index. */
R_TRY(this->index.Initialize(capacity));
return ResultSuccess();
}
size_t GetCount() const {
return this->index.GetCount();
}
size_t GetCapacity() const {
return this->index.GetCapacity();
}
Result Load() {
/* Reset any existing entries. */
this->index.ResetEntries();
/* Try to read the archive -- note, path not found is a success condition. */
/* This is because no archive file = no entries, so we're in the right state. */
AutoBuffer buffer;
R_TRY_CATCH(this->ReadArchiveFile(&buffer)) {
R_CONVERT(fs::ResultPathNotFound, ResultSuccess());
} R_END_TRY_CATCH;
/* Parse entries from the buffer. */
{
ArchiveReader reader(buffer);
size_t entry_count = 0;
R_TRY(reader.ReadEntryCount(&entry_count));
for (size_t i = 0; i < entry_count; i++) {
/* Get size of key/value. */
size_t key_size = 0, value_size = 0;
R_TRY(reader.GetEntrySize(&key_size, &value_size));
/* Allocate memory for value. */
void *new_value = std::malloc(value_size);
R_UNLESS(new_value != nullptr, ResultAllocationFailed());
auto value_guard = SCOPE_GUARD { std::free(new_value); };
/* Read key and value. */
Key key;
R_TRY(reader.ReadEntry(&key, sizeof(key), new_value, value_size));
R_TRY(this->index.AddUnsafe(key, new_value, value_size));
/* We succeeded, so cancel the value guard to prevent deallocation. */
value_guard.Cancel();
}
}
return ResultSuccess();
}
Result Save() {
/* Create a buffer to hold the archive. */
AutoBuffer buffer;
R_TRY(buffer.Initialize(this->GetArchiveSize()));
/* Write the archive to the buffer. */
{
ArchiveWriter writer(buffer);
writer.WriteHeader(this->GetCount());
for (const auto &it : this->index) {
const auto &key = it.GetKey();
writer.WriteEntry(&key, sizeof(Key), it.GetValuePointer(), it.GetValueSize());
}
}
/* Save the buffer to disk. */
return this->Commit(buffer);
}
Result Set(const Key &key, const void *value, size_t value_size) {
return this->index.Set(key, value, value_size);
}
template<typename Value>
Result Set(const Key &key, const Value &value) {
/* Only allow setting pod. */
static_assert(std::is_pod<Value>::value, "KeyValueStore Values must be pod");
return this->Set(key, &value, sizeof(Value));
}
template<typename Value>
Result Set(const Key &key, const Value *value) {
/* Only allow setting pod. */
static_assert(std::is_pod<Value>::value, "KeyValueStore Values must be pod");
return this->Set(key, value, sizeof(Value));
}
Result Get(size_t *out_size, void *out_value, size_t max_out_size, const Key &key) {
/* Find entry. */
auto it = this->find(key);
R_UNLESS(it != this->end(), ResultKeyNotFound());
size_t size = std::min(max_out_size, it->GetValueSize());
std::memcpy(out_value, it->GetValuePointer(), size);
*out_size = size;
return ResultSuccess();
}
template<typename Value = void>
Result GetValuePointer(Value **out_value, const Key &key) {
/* Find entry. */
auto it = this->find(key);
R_UNLESS(it != this->end(), ResultKeyNotFound());
*out_value = it->template GetValuePointer<Value>();
return ResultSuccess();
}
template<typename Value = void>
Result GetValuePointer(const Value **out_value, const Key &key) const {
/* Find entry. */
auto it = this->find(key);
R_UNLESS(it != this->end(), ResultKeyNotFound());
*out_value = it->template GetValuePointer<Value>();
return ResultSuccess();
}
template<typename Value>
Result GetValue(Value *out_value, const Key &key) const {
/* Find entry. */
auto it = this->find(key);
R_UNLESS(it != this->end(), ResultKeyNotFound());
*out_value = it->template GetValue<Value>();
return ResultSuccess();
}
Result GetValueSize(size_t *out_size, const Key &key) const {
/* Find entry. */
auto it = this->find(key);
R_UNLESS(it != this->end(), ResultKeyNotFound());
*out_size = it->GetValueSize();
return ResultSuccess();
}
Result Remove(const Key &key) {
return this->index.Remove(key);
}
Entry *begin() {
return this->index.begin();
}
const Entry *begin() const {
return this->index.begin();
}
Entry *end() {
return this->index.end();
}
const Entry *end() const {
return this->index.end();
}
const Entry *cbegin() const {
return this->index.cbegin();
}
const Entry *cend() const {
return this->index.cend();
}
Entry *lower_bound(const Key &key) {
return this->index.lower_bound(key);
}
const Entry *lower_bound(const Key &key) const {
return this->index.lower_bound(key);
}
Entry *find(const Key &key) {
return this->index.find(key);
}
const Entry *find(const Key &key) const {
return this->index.find(key);
}
private:
Result Commit(const AutoBuffer &buffer) {
/* Try to delete temporary archive, but allow deletion failure (it may not exist). */
std::remove(this->temp_path.Get());
/* Create new temporary archive. */
R_TRY(fsdevCreateFile(this->temp_path.Get(), buffer.GetSize(), 0));
/* Write data to the temporary archive. */
{
FILE *f = fopen(this->temp_path, "r+b");
R_UNLESS(f != nullptr, fsdevGetLastResult());
ON_SCOPE_EXIT { fclose(f); };
R_UNLESS(fwrite(buffer.Get(), buffer.GetSize(), 1, f) == 1, fsdevGetLastResult());
}
/* Try to delete the saved archive, but allow deletion failure. */
std::remove(this->path.Get());
/* Rename the path. */
R_UNLESS(std::rename(this->temp_path.Get(), this->path.Get()) == 0, fsdevGetLastResult());
return ResultSuccess();
}
size_t GetArchiveSize() const {
ArchiveSizeHelper size_helper;
for (const auto &it : this->index) {
size_helper.AddEntry(sizeof(Key), it.GetValueSize());
}
return size_helper.GetSize();
}
Result ReadArchiveFile(AutoBuffer *dst) const {
/* Open the file. */
FILE *f = fopen(this->path, "rb");
R_UNLESS(f != nullptr, fsdevGetLastResult());
ON_SCOPE_EXIT { fclose(f); };
/* Get the archive file size. */
fseek(f, 0, SEEK_END);
const size_t archive_size = ftell(f);
fseek(f, 0, SEEK_SET);
/* Make a new buffer, read the file. */
R_TRY(dst->Initialize(archive_size));
R_UNLESS(fread(dst->Get(), archive_size, 1, f) == 1, fsdevGetLastResult());
return ResultSuccess();
}
};
}