CoriEngine
Loading...
Searching...
No Matches
BinaryFileManager.hpp
Go to the documentation of this file.
1#pragma once
2#include "Core/Uuid.hpp"
4
5namespace Cori {
6 namespace FileSystem {
7 namespace Internal {
8 template <typename T>
9 struct is_vector : std::false_type {};
10
11 template <typename T, typename Alloc>
12 struct is_vector<std::vector<T, Alloc>> : std::true_type {};
13
14 template <typename T>
16
17 template <typename T>
18 concept Trivial = std::is_trivial_v<T> && !IsVector<T>;
19 }
20
34 public:
42 template<typename T> requires std::is_aggregate_v<T>
43 static void SaveAggregateStruct(const T& data, const std::filesystem::path& file, const bool safeMode = false) {
44 auto uuid = Core::UUID().GetRaw();
45
46 Serializer serializer;
47 serializer.Write(data);
48
49 uint64_t structUID = Utility::GetAggregateStructUID<T>();
50 uint64_t checkSum = Utility::fnv1a64(reinterpret_cast<const char*>(serializer.m_Buffer.data()), serializer.m_Buffer.size());
51 size_t size = serializer.m_Buffer.size();
52
53 std::filesystem::path targetFile = std::filesystem::path(file) += ".tmp";
54
55 std::filesystem::create_directories(targetFile.parent_path());
56
57 std::ofstream out(targetFile, std::ios::binary | std::ios::trunc);
58
59 if (!out.good()) {
60 CORI_ERROR_TAGGED({ Logger::Tags::FileSystem::Self, Logger::Tags::FileSystem::BinaryFileManager }, "Failed to open file '{}' for writing, aborting operation.", targetFile.string());
61 return;
62 }
63
64 out.write(reinterpret_cast<const char*>(&uuid), sizeof(uuid));
65 out.write(reinterpret_cast<const char*>(&structUID), sizeof(structUID));
66 out.write(reinterpret_cast<const char*>(&checkSum), sizeof(checkSum));
67 out.write(reinterpret_cast<const char*>(&size), sizeof(size));
68
69 out.write(reinterpret_cast<const char*>(serializer.m_Buffer.data()), serializer.m_Buffer.size());
70
71 out.write(reinterpret_cast<const char*>(&uuid), sizeof(uuid));
72
73 out.close();
74
75 std::filesystem::rename(targetFile, file);
76
77 if (safeMode) {
78 std::filesystem::path bakPath = std::filesystem::path(file) += ".bak";
79
80 std::filesystem::create_directories(bakPath.parent_path());
81
82 std::ofstream outBak(bakPath, std::ios::binary | std::ios::trunc);
83
84 if (!outBak.good()) {
85 CORI_ERROR_TAGGED({ Logger::Tags::FileSystem::Self, Logger::Tags::FileSystem::BinaryFileManager }, "Failed to open file '{}' to creating backup for '{}', aborting operation, backup will not be created!", bakPath.string(), file.string());
86 return;
87 }
88
89 outBak.write(reinterpret_cast<const char*>(&uuid), sizeof(uuid));
90 outBak.write(reinterpret_cast<const char*>(&structUID), sizeof(structUID));
91 outBak.write(reinterpret_cast<const char*>(&checkSum), sizeof(checkSum));
92 outBak.write(reinterpret_cast<const char*>(&size), sizeof(size));
93
94 outBak.write(reinterpret_cast<const char*>(serializer.m_Buffer.data()), serializer.m_Buffer.size());
95
96 outBak.write(reinterpret_cast<const char*>(&uuid), sizeof(uuid));
97
98 outBak.close();
99 }
100 }
101
102
110 template<typename T> requires std::is_aggregate_v<T>
111 static std::expected<T, Core::CoriError<>> LoadAggregateStruct(const std::filesystem::path& file, const bool backupFallback = false) {
112 if (backupFallback) {
113 return LoadAggregateStructImpl<T>(file).or_else([file](const Core::CoriError<>& error) {
116 return LoadAggregateStructImpl<T>(std::filesystem::path(file) += ".bak");
117 });
118 }
119
120 return LoadAggregateStructImpl<T>(file);
121 }
122
123
124 private:
125 template<typename T> requires std::is_aggregate_v<T>
126 static std::expected<T, Core::CoriError<>> LoadAggregateStructImpl(const std::filesystem::path& file) {
127 T data;
128
129 std::ifstream in(file, std::ios::binary);
130
131 if (!in.good()) {
132 return std::unexpected(Core::CoriError(std::format("Failed to open file '{}' for reading.", file.string())));
133 }
134
135 in.seekg(0, std::ios::end);
136 size_t fileSize = in.tellg();
137
138 if (fileSize < m_HeaderSize + m_FooterSize) {
139 return std::unexpected(Core::CoriError(std::format("Failed to read file '{}'. Impossible file size '{}', file is corrupted.", file.string(), fileSize)));
140 }
141
142 in.seekg(0, std::ios::beg);
143
144 std::vector<std::byte> buffer(fileSize);
145 try {
146 in.read(reinterpret_cast<char*>(buffer.data()), fileSize);
147 }
148 catch (const std::exception& e) {
149 return std::unexpected(Core::CoriError(std::format("Failed to read from file '{}', error '{}'.", file.string(), e.what())));
150 }
151
152 auto headerUUID = *reinterpret_cast<std::pair<uint64_t, uint64_t>*>(&buffer[0]);
153 auto footerUUID = *reinterpret_cast<std::pair<uint64_t, uint64_t>*>(&buffer[fileSize - m_FooterSize]);
154 auto readStructUID = *reinterpret_cast<uint64_t*>(&buffer[16]);
155 auto readCheckSum = *reinterpret_cast<uint64_t*>(&buffer[24]);
156 auto readDataBlockSize = *reinterpret_cast<size_t*>(&buffer[32]);
157
158 if (readDataBlockSize + m_HeaderSize + m_FooterSize != fileSize) {
159 return std::unexpected(Core::CoriError(std::format("Failed to load file '{}'. Expected (cached) file size '{}' does not match the actual file size '{}'. File is corrupted.", file.string(), readDataBlockSize + m_HeaderSize + m_FooterSize, fileSize)));
160 }
161
162 if (headerUUID.first != footerUUID.first || headerUUID.second != footerUUID.second) {
163 return std::unexpected(Core::CoriError(std::format("Failed to load file '{}'. UUID in the header doesn't match UUID in the footer, usually happens when program began writing a file, but then crashed/got closen without finishing properly. File is corrupted.", file.string())));
164 }
165
166 uint64_t structUID = Utility::GetAggregateStructUID<T>();
167 if (readStructUID != structUID) {
168 return std::unexpected(Core::CoriError(std::format("Failed to load file '{}'. Expected (cached) struct UID '{}' doesn't match the struct UID '{}' (typename <{}>) you're trying to write the contents to. You either trying to load into a incorrect struct type, or the file is corrupted.", file.string(), readStructUID, structUID, CORI_CLEAN_TYPE_NAME(T))));
169 }
170
171 uint64_t checkSum = Utility::fnv1a64(reinterpret_cast<const char*>(buffer.data() + m_HeaderSize), readDataBlockSize);
172
173 if (readCheckSum != checkSum) {
174 return std::unexpected(Core::CoriError(std::format("Failed to load file '{}'. Expected (cached) check sum '{}' doesn't match the check sum '{}' of the loaded data block. File is corrupted.", file.string(), readCheckSum, checkSum)));
175 }
176
177 Deserializer deserializer(buffer, m_HeaderSize);
178 deserializer.Read(data);
179 return data;
180 }
181
182 class Serializer {
183 public:
184 template <typename T>
185 void Write(const T& value) {
186 if constexpr (Internal::Trivial<T>) {
187 const auto* ptr = reinterpret_cast<const std::byte*>(&value);
188 m_Buffer.insert(m_Buffer.end(), ptr, ptr + sizeof(T));
189 }
190 else if constexpr (Internal::IsVector<T>) {
191 Write(value.size());
192 const auto* ptr = reinterpret_cast<const std::byte*>(value.data());
193 m_Buffer.insert(m_Buffer.end(), ptr, ptr + value.size() * sizeof(typename T::value_type));
194 }
195 else if constexpr (std::is_aggregate_v<T>) {
196 boost::pfr::for_each_field(value, [this](const auto& field) {
197 this->Write(field);
198 });
199 } else {
200 static_assert(!sizeof(T), "Unsupported type for serialization");
201 }
202 }
203
204 std::vector<std::byte> m_Buffer;
205 };
206
207 class Deserializer {
208 public:
209 explicit Deserializer(const std::vector<std::byte>& buffer, const size_t startingOffset) : m_Buffer(buffer), m_Offset(startingOffset) {}
210
211 template <typename T>
212 void Read(T& value) {
213 if (m_Offset >= m_Buffer.size()) {
214 throw std::runtime_error("Read past end of buffer.");
215 }
216
217 if constexpr (Internal::Trivial<T>) {
218 std::memcpy(&value, m_Buffer.data() + m_Offset, sizeof(T));
219 m_Offset += sizeof(T);
220 }
221 else if constexpr (Internal::IsVector<T>) {
222 size_t size;
223 Read(size);
224 value.resize(size);
225 const size_t dataBlockSize = size * sizeof(typename T::value_type);
226 std::memcpy(value.data(), m_Buffer.data() + m_Offset, dataBlockSize);
227 m_Offset += dataBlockSize;
228 }
229 else if constexpr (std::is_aggregate_v<T>) {
230 boost::pfr::for_each_field(value, [this](auto& field) {
231 this->Read(field);
232 });
233 } else {
234 static_assert(!sizeof(T), "Unsupported type for deserialization");
235 }
236 }
237
238 private:
239 const std::vector<std::byte>& m_Buffer;
240 size_t m_Offset;
241 };
242
243 static constexpr size_t m_HeaderSize = 40;
244 static constexpr size_t m_FooterSize = 16;
245
246 };
247 }
248}
#define CORI_CLEAN_TYPE_NAME(tn)
#define CORI_CORE_ERROR_TAGGED(...)
Definition Logger.hpp:1039
#define CORI_ERROR_TAGGED(...)
Definition Logger.hpp:1063
Custom error class mainly used in std::expected.
Definition Error.hpp:27
const char * what() const noexcept override
Returns the formated message and if called the CoriError is considered 'seen'.
Definition Error.hpp:74
A 128bit UUID, can be serialized to the string and deserialized from it.
Definition Uuid.hpp:10
std::pair< uint64_t, uint64_t > GetRaw() const
Definition Uuid.hpp:33
Saves and loads aggregate structs to/from drive.
static std::expected< T, Core::CoriError<> > LoadAggregateStruct(const std::filesystem::path &file, const bool backupFallback=false)
Loads an aggregate struct from the file on the disk.
static void SaveAggregateStruct(const T &data, const std::filesystem::path &file, const bool safeMode=false)
Saves an instance of an aggregate struct onto the disk.
Everything connected to interacting with files and filesystem is in this namespace.
constexpr std::uint64_t fnv1a64(const char *str, const std::size_t len)
Computes the 64bit FNV-1a string hash.
Definition StringHash.hpp:9
consteval uint64_t GetAggregateStructUID()
Generates a stable, unique ID for an aggregate struct at compile-time.
Global engine namespace.
static constexpr char Self[]
Definition Logger.hpp:105
static constexpr char BinaryFileManager[]
Definition Logger.hpp:107