mirror of
				https://github.com/Atmosphere-NX/Atmosphere-libs.git
				synced 2025-11-04 13:21:17 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			383 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			383 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
/*
 | 
						|
 * Copyright (c) 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 <stratosphere/fs/fs_filesystem.hpp>
 | 
						|
#include <stratosphere/fs/fs_file.hpp>
 | 
						|
#include <stratosphere/kvdb/kvdb_bounded_string.hpp>
 | 
						|
#include <stratosphere/kvdb/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 m_file_path;
 | 
						|
                Key *m_keys;
 | 
						|
                LruHeader m_header;
 | 
						|
            public:
 | 
						|
                static Result CreateNewList(const char *path) {
 | 
						|
                    /* Create new lru_list.dat. */
 | 
						|
                    R_TRY(fs::CreateFile(path, FileSize));
 | 
						|
 | 
						|
                    /* Open the file. */
 | 
						|
                    fs::FileHandle file;
 | 
						|
                    R_TRY(fs::OpenFile(std::addressof(file), path, fs::OpenMode_Write));
 | 
						|
                    ON_SCOPE_EXIT { fs::CloseFile(file); };
 | 
						|
 | 
						|
                    /* Write new header with zero entries to the file. */
 | 
						|
                    LruHeader new_header = { .entry_count = 0, };
 | 
						|
                    R_TRY(fs::WriteFile(file, 0, std::addressof(new_header), sizeof(new_header), fs::WriteOption::Flush));
 | 
						|
 | 
						|
                    R_SUCCEED();
 | 
						|
                }
 | 
						|
            private:
 | 
						|
                void RemoveIndex(size_t i) {
 | 
						|
                    AMS_ABORT_UNLESS(i < this->GetCount());
 | 
						|
                    std::memmove(m_keys + i, m_keys + i + 1, sizeof(*m_keys) * (this->GetCount() - (i + 1)));
 | 
						|
                    this->DecrementCount();
 | 
						|
                }
 | 
						|
 | 
						|
                void IncrementCount() {
 | 
						|
                    m_header.entry_count++;
 | 
						|
                }
 | 
						|
 | 
						|
                void DecrementCount() {
 | 
						|
                    m_header.entry_count--;
 | 
						|
                }
 | 
						|
            public:
 | 
						|
                LruList() : m_keys(nullptr), m_header() { /* ... */ }
 | 
						|
 | 
						|
                Result Initialize(const char *path, void *buf, size_t size) {
 | 
						|
                    /* Only initialize once, and ensure we have sufficient memory. */
 | 
						|
                    AMS_ABORT_UNLESS(m_keys == nullptr);
 | 
						|
                    AMS_ABORT_UNLESS(size >= BufferSize);
 | 
						|
 | 
						|
                    /* Setup member variables. */
 | 
						|
                    m_keys = static_cast<Key *>(buf);
 | 
						|
                    m_file_path.Assign(path);
 | 
						|
                    std::memset(m_keys, 0, BufferSize);
 | 
						|
 | 
						|
                    /* Open file. */
 | 
						|
                    fs::FileHandle file;
 | 
						|
                    R_TRY(fs::OpenFile(std::addressof(file), m_file_path, fs::OpenMode_Read));
 | 
						|
                    ON_SCOPE_EXIT { fs::CloseFile(file); };
 | 
						|
 | 
						|
                    /* Read header. */
 | 
						|
                    R_TRY(fs::ReadFile(file, 0, std::addressof(m_header), sizeof(m_header)));
 | 
						|
 | 
						|
                    /* Read entries. */
 | 
						|
                    R_TRY(fs::ReadFile(file, sizeof(m_header), m_keys, BufferSize));
 | 
						|
 | 
						|
                    R_SUCCEED();
 | 
						|
                }
 | 
						|
 | 
						|
                Result Save() {
 | 
						|
                    /* Open file. */
 | 
						|
                    fs::FileHandle file;
 | 
						|
                    R_TRY(fs::OpenFile(std::addressof(file), m_file_path, fs::OpenMode_Read));
 | 
						|
                    ON_SCOPE_EXIT { fs::CloseFile(file); };
 | 
						|
 | 
						|
                    /* Write header. */
 | 
						|
                    R_TRY(fs::WriteFile(file, 0, std::addressof(m_header), sizeof(m_header), fs::WriteOption::None));
 | 
						|
 | 
						|
                    /* Write entries. */
 | 
						|
                    R_TRY(fs::WriteFile(file, sizeof(m_header), m_keys, BufferSize, fs::WriteOption::None));
 | 
						|
 | 
						|
                    /* Flush. */
 | 
						|
                    R_TRY(fs::FlushFile(file));
 | 
						|
 | 
						|
                    R_SUCCEED();
 | 
						|
                }
 | 
						|
 | 
						|
                size_t GetCount() const {
 | 
						|
                    return m_header.entry_count;
 | 
						|
                }
 | 
						|
 | 
						|
                bool IsEmpty() const {
 | 
						|
                    return this->GetCount() == 0;
 | 
						|
                }
 | 
						|
 | 
						|
                bool IsFull() const {
 | 
						|
                    return this->GetCount() >= Capacity;
 | 
						|
                }
 | 
						|
 | 
						|
                Key Get(size_t i) const {
 | 
						|
                    AMS_ABORT_UNLESS(i < this->GetCount());
 | 
						|
                    return m_keys[i];
 | 
						|
                }
 | 
						|
 | 
						|
                Key Peek() const {
 | 
						|
                    AMS_ABORT_UNLESS(!this->IsEmpty());
 | 
						|
                    return this->Get(0);
 | 
						|
                }
 | 
						|
 | 
						|
                void Push(const Key &key) {
 | 
						|
                    AMS_ABORT_UNLESS(!this->IsFull());
 | 
						|
                    m_keys[this->GetCount()] = key;
 | 
						|
                    this->IncrementCount();
 | 
						|
                }
 | 
						|
 | 
						|
                Key Pop() {
 | 
						|
                    AMS_ABORT_UNLESS(!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 (m_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 (m_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(util::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 m_kvs;
 | 
						|
            LeastRecentlyUsedList m_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, fs::DirectoryEntryType type) {
 | 
						|
                /* Set out to false initially. */
 | 
						|
                *out = false;
 | 
						|
 | 
						|
                /* Try to get the entry type. */
 | 
						|
                fs::DirectoryEntryType entry_type;
 | 
						|
                R_TRY_CATCH(fs::GetEntryType(std::addressof(entry_type), path)) {
 | 
						|
                    /* If the path doesn't exist, nothing has gone wrong. */
 | 
						|
                    R_CONVERT(fs::ResultPathNotFound, ResultSuccess());
 | 
						|
                } R_END_TRY_CATCH;
 | 
						|
 | 
						|
                /* Check that the entry type is correct. */
 | 
						|
                R_UNLESS(entry_type == type, kvdb::ResultInvalidFilesystemState());
 | 
						|
 | 
						|
                /* The entry exists and is the correct type. */
 | 
						|
                *out = true;
 | 
						|
                R_SUCCEED();
 | 
						|
            }
 | 
						|
 | 
						|
            static Result DirectoryExists(bool *out, const char *path) {
 | 
						|
                R_RETURN(Exists(out, path, fs::DirectoryEntryType_Directory));
 | 
						|
            }
 | 
						|
 | 
						|
            static Result FileExists(bool *out, const char *path) {
 | 
						|
                R_RETURN(Exists(out, path, fs::DirectoryEntryType_File));
 | 
						|
            }
 | 
						|
        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_TRY(fs::CreateDirectory(dir));
 | 
						|
 | 
						|
                R_SUCCEED();
 | 
						|
            }
 | 
						|
 | 
						|
            static Result ValidateExistingCache(const char *dir) {
 | 
						|
                /* Check for existence. */
 | 
						|
                bool has_lru = false, has_kvs = false;
 | 
						|
                R_TRY(FileExists(std::addressof(has_lru), GetLeastRecentlyUsedListPath(dir)));
 | 
						|
                R_TRY(DirectoryExists(std::addressof(has_kvs), GetFileKeyValueStorePath(dir)));
 | 
						|
 | 
						|
                /* If neither exists, CreateNewCache was never called. */
 | 
						|
                R_UNLESS(has_lru || has_kvs, kvdb::ResultNotCreated());
 | 
						|
 | 
						|
                /* If one exists but not the other, we have an invalid state. */
 | 
						|
                R_UNLESS(has_lru && has_kvs, kvdb::ResultInvalidFilesystemState());
 | 
						|
 | 
						|
                R_SUCCEED();
 | 
						|
            }
 | 
						|
        private:
 | 
						|
            void RemoveOldestKey() {
 | 
						|
                const Key &oldest_key = m_lru_list.Peek();
 | 
						|
                m_lru_list.Pop();
 | 
						|
                m_kvs.Remove(oldest_key);
 | 
						|
            }
 | 
						|
        public:
 | 
						|
            Result Initialize(const char *dir, void *buf, size_t size) {
 | 
						|
                /* Initialize list. */
 | 
						|
                R_TRY(m_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(m_kvs.Initialize(dir));
 | 
						|
 | 
						|
                R_SUCCEED();
 | 
						|
            }
 | 
						|
 | 
						|
            size_t GetCount() const {
 | 
						|
                return m_lru_list.GetCount();
 | 
						|
            }
 | 
						|
 | 
						|
            size_t GetCapacity() const {
 | 
						|
                return Capacity;
 | 
						|
            }
 | 
						|
 | 
						|
            Key GetKey(size_t i) const {
 | 
						|
                return m_lru_list.Get(i);
 | 
						|
            }
 | 
						|
 | 
						|
            bool Contains(const Key &key) const {
 | 
						|
                return m_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. */
 | 
						|
                m_lru_list.Update(key);
 | 
						|
                R_RETURN(m_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. */
 | 
						|
                m_lru_list.Update(key);
 | 
						|
                R_RETURN(m_kvs.Get(out_value, key));
 | 
						|
            }
 | 
						|
 | 
						|
            Result GetSize(size_t *out_size, const Key &key) {
 | 
						|
                R_RETURN(m_kvs.GetSize(out_size, key));
 | 
						|
            }
 | 
						|
 | 
						|
            Result Set(const Key &key, const void *value, size_t value_size) {
 | 
						|
                if (m_lru_list.Update(key)) {
 | 
						|
                    /* If an entry for the key exists, delete the existing value file. */
 | 
						|
                    m_kvs.Remove(key);
 | 
						|
                } else {
 | 
						|
                    /* If the list is full, we need to remove the oldest key. */
 | 
						|
                    if (m_lru_list.IsFull()) {
 | 
						|
                        this->RemoveOldestKey();
 | 
						|
                    }
 | 
						|
 | 
						|
                    /* Add the key to the list. */
 | 
						|
                    m_lru_list.Push(key);
 | 
						|
                }
 | 
						|
 | 
						|
                /* Loop, trying to save the new value to disk. */
 | 
						|
                while (true) {
 | 
						|
                    /* Try to set the key. */
 | 
						|
                    R_TRY_CATCH(m_kvs.Set(key, value, value_size)) {
 | 
						|
                        R_CATCH(fs::ResultNotEnoughFreeSpace) {
 | 
						|
                            /* If our entry is the only thing in the Lru list, remove it. */
 | 
						|
                            if (m_lru_list.GetCount() == 1) {
 | 
						|
                                m_lru_list.Pop();
 | 
						|
                                R_TRY(m_lru_list.Save());
 | 
						|
                                R_THROW(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(m_lru_list.Save());
 | 
						|
 | 
						|
                R_SUCCEED();
 | 
						|
            }
 | 
						|
 | 
						|
            template<typename Value>
 | 
						|
            Result Set(const Key &key, const Value &value) {
 | 
						|
                R_RETURN(this->Set(key, &value, sizeof(Value)));
 | 
						|
            }
 | 
						|
 | 
						|
            Result Remove(const Key &key) {
 | 
						|
                /* Remove the key. */
 | 
						|
                m_lru_list.Remove(key);
 | 
						|
                R_TRY(m_kvs.Remove(key));
 | 
						|
                R_TRY(m_lru_list.Save());
 | 
						|
 | 
						|
                R_SUCCEED();
 | 
						|
            }
 | 
						|
 | 
						|
            Result RemoveAll() {
 | 
						|
                /* TODO: Nintendo doesn't check errors here. Should we? */
 | 
						|
                while (!m_lru_list.IsEmpty()) {
 | 
						|
                    this->RemoveOldestKey();
 | 
						|
                }
 | 
						|
                R_TRY(m_lru_list.Save());
 | 
						|
 | 
						|
                R_SUCCEED();
 | 
						|
            }
 | 
						|
    };
 | 
						|
 | 
						|
}
 |