1/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
2/* If you are missing that file, acquire a complete release at teeworlds.com. */
3
4#include "datafile.h"
5
6#include "uuid_manager.h"
7
8#include <base/hash_ctxt.h>
9#include <base/log.h>
10#include <base/math.h>
11#include <base/system.h>
12
13#include <engine/storage.h>
14
15#include <zlib.h>
16
17#include <cstdlib>
18#include <limits>
19#include <unordered_set>
20
21static constexpr int MAX_ITEM_TYPE = 0xFFFF;
22static constexpr int MAX_ITEM_ID = 0xFFFF;
23static constexpr int OFFSET_UUID_TYPE = 0x8000;
24
25static inline void SwapEndianInPlace(void *pObj, size_t Size)
26{
27#if defined(CONF_ARCH_ENDIAN_BIG)
28 swap_endian(pObj, sizeof(int), Size / sizeof(int));
29#endif
30}
31
32template<typename T>
33static inline void SwapEndianInPlace(T *pObj)
34{
35 static_assert(sizeof(T) % sizeof(int) == 0);
36 SwapEndianInPlace(pObj, sizeof(T));
37}
38
39static inline int SwapEndianInt(int Number)
40{
41 SwapEndianInPlace(pObj: &Number);
42 return Number;
43}
44
45class CItemEx
46{
47public:
48 int m_aUuid[sizeof(CUuid) / sizeof(int32_t)];
49
50 static CItemEx FromUuid(CUuid Uuid)
51 {
52 CItemEx Result;
53 for(size_t i = 0; i < std::size(Result.m_aUuid); i++)
54 {
55 Result.m_aUuid[i] = bytes_be_to_uint(bytes: &Uuid.m_aData[i * sizeof(int32_t)]);
56 }
57 return Result;
58 }
59
60 CUuid ToUuid() const
61 {
62 CUuid Result;
63 for(size_t i = 0; i < std::size(m_aUuid); i++)
64 {
65 uint_to_bytes_be(bytes: &Result.m_aData[i * sizeof(int32_t)], value: m_aUuid[i]);
66 }
67 return Result;
68 }
69};
70
71class CDatafileItemType
72{
73public:
74 int m_Type;
75 int m_Start;
76 int m_Num;
77};
78
79class CDatafileItem
80{
81public:
82 unsigned m_TypeAndId;
83 int m_Size;
84
85 int Type() const
86 {
87 return (m_TypeAndId >> 16u) & MAX_ITEM_TYPE;
88 }
89
90 int Id() const
91 {
92 return m_TypeAndId & MAX_ITEM_ID;
93 }
94};
95
96class CDatafileHeader
97{
98public:
99 char m_aId[4];
100 int m_Version;
101 int m_Size;
102 int m_Swaplen;
103 int m_NumItemTypes;
104 int m_NumItems;
105 int m_NumRawData;
106 int m_ItemSize;
107 int m_DataSize;
108
109 constexpr size_t SizeOffset()
110 {
111 // The size of these members is not included in m_Size and m_Swaplen
112 return sizeof(m_aId) + sizeof(m_Version) + sizeof(m_Size) + sizeof(m_Swaplen);
113 }
114};
115
116class CDatafileInfo
117{
118public:
119 CDatafileItemType *m_pItemTypes;
120 int *m_pItemOffsets;
121 int *m_pDataOffsets;
122 int *m_pDataSizes;
123
124 char *m_pItemStart;
125 char *m_pDataStart;
126};
127
128class CDatafile
129{
130public:
131 IOHANDLE m_File;
132 unsigned m_FileSize;
133 SHA256_DIGEST m_Sha256;
134 unsigned m_Crc;
135 CDatafileInfo m_Info;
136 CDatafileHeader m_Header;
137 int m_DataStartOffset;
138 void **m_ppDataPtrs;
139 int *m_pDataSizes;
140 char *m_pData;
141
142 int GetFileDataSize(int Index) const
143 {
144 dbg_assert(Index >= 0 && Index < m_Header.m_NumRawData, "Invalid Index: %d", Index);
145
146 if(Index == m_Header.m_NumRawData - 1)
147 {
148 return m_Header.m_DataSize - m_Info.m_pDataOffsets[Index];
149 }
150
151 return m_Info.m_pDataOffsets[Index + 1] - m_Info.m_pDataOffsets[Index];
152 }
153
154 int GetDataSize(int Index) const
155 {
156 // Invalid data indices may appear in map items
157 if(Index < 0 || Index >= m_Header.m_NumRawData)
158 {
159 return 0;
160 }
161
162 if(m_ppDataPtrs[Index] == nullptr)
163 {
164 if(m_Info.m_pDataSizes != nullptr)
165 {
166 return m_Info.m_pDataSizes[Index];
167 }
168 else
169 {
170 return GetFileDataSize(Index);
171 }
172 }
173
174 const int Size = m_pDataSizes[Index];
175 if(Size < 0)
176 {
177 return 0; // summarize all errors as zero size
178 }
179 return Size;
180 }
181
182 void *GetData(int Index, bool Swap) const
183 {
184 // Invalid data indices may appear in map items
185 if(Index < 0 || Index >= m_Header.m_NumRawData)
186 {
187 return nullptr;
188 }
189
190 // Data already loaded
191 if(m_ppDataPtrs[Index] != nullptr)
192 {
193 return m_ppDataPtrs[Index];
194 }
195
196 // Don't try to load the data again if it previously failed
197 if(m_pDataSizes[Index] < 0)
198 {
199 return nullptr;
200 }
201
202 const unsigned DataSize = GetFileDataSize(Index);
203 if(m_Info.m_pDataSizes != nullptr)
204 {
205 // v4 has compressed data
206 const unsigned OriginalUncompressedSize = m_Info.m_pDataSizes[Index];
207 log_trace("datafile", "loading data. index=%d size=%d uncompressed=%d", Index, DataSize, OriginalUncompressedSize);
208 if(OriginalUncompressedSize == 0)
209 {
210 log_error("datafile", "data size invalid. data will be ignored. index=%d size=%d uncompressed=%d", Index, DataSize, OriginalUncompressedSize);
211 m_ppDataPtrs[Index] = nullptr;
212 m_pDataSizes[Index] = -1;
213 return nullptr;
214 }
215
216 // read the compressed data
217 void *pCompressedData = malloc(size: DataSize);
218 if(pCompressedData == nullptr)
219 {
220 log_error("datafile", "out of memory. could not allocate memory for compressed data. index=%d size=%d", Index, DataSize);
221 m_ppDataPtrs[Index] = nullptr;
222 m_pDataSizes[Index] = -1;
223 return nullptr;
224 }
225 unsigned ActualDataSize = 0;
226 if(io_seek(io: m_File, offset: m_DataStartOffset + m_Info.m_pDataOffsets[Index], origin: IOSEEK_START) == 0)
227 {
228 ActualDataSize = io_read(io: m_File, buffer: pCompressedData, size: DataSize);
229 }
230 if(DataSize != ActualDataSize)
231 {
232 log_error("datafile", "truncation error. could not read all compressed data. index=%d wanted=%d got=%d", Index, DataSize, ActualDataSize);
233 free(ptr: pCompressedData);
234 m_ppDataPtrs[Index] = nullptr;
235 m_pDataSizes[Index] = -1;
236 return nullptr;
237 }
238
239 // decompress the data
240 m_ppDataPtrs[Index] = static_cast<char *>(malloc(size: OriginalUncompressedSize));
241 if(m_ppDataPtrs[Index] == nullptr)
242 {
243 free(ptr: pCompressedData);
244 log_error("datafile", "out of memory. could not allocate memory for uncompressed data. index=%d size=%d", Index, OriginalUncompressedSize);
245 m_pDataSizes[Index] = -1;
246 return nullptr;
247 }
248 unsigned long UncompressedSize = OriginalUncompressedSize;
249 const int Result = uncompress(dest: static_cast<Bytef *>(m_ppDataPtrs[Index]), destLen: &UncompressedSize, source: static_cast<Bytef *>(pCompressedData), sourceLen: DataSize);
250 free(ptr: pCompressedData);
251 if(Result != Z_OK || UncompressedSize != OriginalUncompressedSize)
252 {
253 log_error("datafile", "failed to uncompress data. index=%d result=%d wanted=%d got=%ld", Index, Result, OriginalUncompressedSize, UncompressedSize);
254 free(ptr: m_ppDataPtrs[Index]);
255 m_ppDataPtrs[Index] = nullptr;
256 m_pDataSizes[Index] = -1;
257 return nullptr;
258 }
259 m_pDataSizes[Index] = OriginalUncompressedSize;
260 }
261 else
262 {
263 log_trace("datafile", "loading data. index=%d size=%d", Index, DataSize);
264 m_ppDataPtrs[Index] = malloc(size: DataSize);
265 if(m_ppDataPtrs[Index] == nullptr)
266 {
267 log_error("datafile", "out of memory. could not allocate memory for uncompressed data. index=%d size=%d", Index, DataSize);
268 m_pDataSizes[Index] = -1;
269 return nullptr;
270 }
271 unsigned ActualDataSize = 0;
272 if(io_seek(io: m_File, offset: m_DataStartOffset + m_Info.m_pDataOffsets[Index], origin: IOSEEK_START) == 0)
273 {
274 ActualDataSize = io_read(io: m_File, buffer: m_ppDataPtrs[Index], size: DataSize);
275 }
276 if(DataSize != ActualDataSize)
277 {
278 log_error("datafile", "truncation error. could not read all uncompressed data. index=%d wanted=%d got=%d", Index, DataSize, ActualDataSize);
279 free(ptr: m_ppDataPtrs[Index]);
280 m_ppDataPtrs[Index] = nullptr;
281 m_pDataSizes[Index] = -1;
282 return nullptr;
283 }
284 m_pDataSizes[Index] = DataSize;
285 }
286 if(Swap)
287 {
288 SwapEndianInPlace(pObj: m_ppDataPtrs[Index], Size: m_pDataSizes[Index]);
289 }
290 return m_ppDataPtrs[Index];
291 }
292
293 int GetFileItemSize(int Index) const
294 {
295 dbg_assert(Index >= 0 && Index < m_Header.m_NumItems, "Invalid Index: %d", Index);
296
297 if(Index == m_Header.m_NumItems - 1)
298 {
299 return m_Header.m_ItemSize - m_Info.m_pItemOffsets[Index];
300 }
301
302 return m_Info.m_pItemOffsets[Index + 1] - m_Info.m_pItemOffsets[Index];
303 }
304
305 int GetItemSize(int Index) const
306 {
307 return GetFileItemSize(Index) - sizeof(CDatafileItem);
308 }
309
310 CDatafileItem *GetItem(int Index) const
311 {
312 dbg_assert(Index >= 0 && Index < m_Header.m_NumItems, "Invalid Index: %d", Index);
313
314 return static_cast<CDatafileItem *>(static_cast<void *>(m_Info.m_pItemStart + m_Info.m_pItemOffsets[Index]));
315 }
316
317 bool Validate() const
318 {
319#define Check(Test, ErrorMessage, ...) \
320 do \
321 { \
322 if(!(Test)) \
323 { \
324 log_error("datafile", "invalid file information: " ErrorMessage, ##__VA_ARGS__); \
325 return false; \
326 } \
327 } while(false)
328
329 // validate item types
330 int64_t CountedItems = 0;
331 std::unordered_set<int> UsedItemTypes;
332 for(int Index = 0; Index < m_Header.m_NumItemTypes; Index++)
333 {
334 const CDatafileItemType &ItemType = m_Info.m_pItemTypes[Index];
335 Check(ItemType.m_Type >= 0 && ItemType.m_Type <= MAX_ITEM_TYPE, "item type has invalid type. index=%d type=%d", Index, ItemType.m_Type);
336 const auto [_, Inserted] = UsedItemTypes.insert(x: ItemType.m_Type);
337 Check(Inserted, "item type has duplicate type. index=%d type=%d", Index, ItemType.m_Type);
338 Check(ItemType.m_Num > 0, "item type has invalid number of items. index=%d type=%d num=%d", Index, ItemType.m_Type, ItemType.m_Num);
339 Check(ItemType.m_Start == CountedItems, "item type has invalid start. index=%d type=%d start=%d", Index, ItemType.m_Type, ItemType.m_Start);
340 CountedItems += ItemType.m_Num;
341 if(CountedItems > m_Header.m_NumItems)
342 {
343 break;
344 }
345 }
346 Check(CountedItems == m_Header.m_NumItems, "mismatched number of items in item types. counted=%" PRId64 " header=%d", CountedItems, m_Header.m_NumItems);
347
348 // validate item offsets
349 int PrevItemOffset = -1;
350 for(int Index = 0; Index < m_Header.m_NumItems; Index++)
351 {
352 const int Offset = m_Info.m_pItemOffsets[Index];
353 if(Index == 0)
354 {
355 Check(Offset == 0, "first item offset is not zero. offset=%d", Offset);
356 }
357 else
358 {
359 Check(Offset > PrevItemOffset, "item offset not greater than previous. index=%d offset=%d previous=%d", Index, Offset, PrevItemOffset);
360 }
361 Check(Offset < m_Header.m_ItemSize, "item offset larger than total item size. index=%d offset=%d total=%d", Index, Offset, m_Header.m_ItemSize);
362 PrevItemOffset = Offset;
363 }
364
365 // validate item sizes, types and IDs
366 int64_t TotalItemSize = 0;
367 for(int TypeIndex = 0; TypeIndex < m_Header.m_NumItemTypes; TypeIndex++)
368 {
369 std::unordered_set<int> UsedItemIds;
370 const CDatafileItemType &ItemType = m_Info.m_pItemTypes[TypeIndex];
371 for(int ItemIndex = ItemType.m_Start; ItemIndex < ItemType.m_Start + ItemType.m_Num; ItemIndex++)
372 {
373 const int FileItemSize = GetFileItemSize(Index: ItemIndex);
374 Check(FileItemSize >= (int)sizeof(CDatafileItem), "map item too small for header. type_index=%d item_index=%d size=%d header=%d", TypeIndex, ItemIndex, FileItemSize, (int)sizeof(CDatafileItem));
375 const CDatafileItem *pItem = GetItem(Index: ItemIndex);
376 Check(pItem->Type() == ItemType.m_Type, "mismatched item type. type_index=%d item_index=%d type=%d expected=%d", TypeIndex, ItemIndex, pItem->Type(), ItemType.m_Type);
377 // Many old maps contain duplicate map items of type ITEMTYPE_EX due to a bug in DDNet tools.
378 if(pItem->Type() != ITEMTYPE_EX)
379 {
380 const auto [_, Inserted] = UsedItemIds.insert(x: pItem->Id());
381 Check(Inserted, "map item has duplicate ID. type_index=%d item_index=%d type=%d ID=%d", TypeIndex, ItemIndex, pItem->Type(), pItem->Id());
382 }
383 Check(pItem->m_Size >= 0, "map item size invalid. type_index=%d item_index=%d size=%d", TypeIndex, ItemIndex, pItem->m_Size);
384 Check(pItem->m_Size % sizeof(int) == 0, "map item size not integer aligned. type_index=%d item_index=%d size=%d", TypeIndex, ItemIndex, pItem->m_Size);
385 Check(pItem->m_Size == GetItemSize(ItemIndex), "map item size does not match file. type_index=%d item_index=%d size=%d file_size=%d", TypeIndex, ItemIndex, pItem->m_Size, GetItemSize(ItemIndex));
386 TotalItemSize += FileItemSize;
387 if(TotalItemSize > m_Header.m_ItemSize)
388 {
389 break;
390 }
391 }
392 }
393 Check(TotalItemSize == m_Header.m_ItemSize, "mismatched total item size. expected=%" PRId64 " header=%d", TotalItemSize, m_Header.m_ItemSize);
394
395 // validate data offsets
396 int PrevDataOffset = -1;
397 for(int Index = 0; Index < m_Header.m_NumRawData; Index++)
398 {
399 const int Offset = m_Info.m_pDataOffsets[Index];
400 if(Index == 0)
401 {
402 Check(Offset == 0, "first data offset must be zero. offset=%d", Offset);
403 }
404 else
405 {
406 Check(Offset > PrevDataOffset, "data offset not greater than previous. index=%d offset=%d previous=%d", Index, Offset, PrevDataOffset);
407 }
408 Check(Offset < m_Header.m_DataSize, "data offset larger than total data size. index=%d offset=%d total=%d", Index, Offset, m_Header.m_DataSize);
409 PrevDataOffset = Offset;
410 }
411
412 // validate data sizes
413 if(m_Info.m_pDataSizes != nullptr)
414 {
415 for(int Index = 0; Index < m_Header.m_NumRawData; Index++)
416 {
417 const int Size = m_Info.m_pDataSizes[Index];
418 Check(Size >= 0, "data size invalid. index=%d size=%d", Index, Size);
419 if(Size == 0)
420 {
421 // Data of size zero is not allowed, but due to existing maps with this quirk we instead allow
422 // the file to be loaded and fail loading the data in the GetData function if the size is zero.
423 log_warn("datafile", "invalid file information: data size invalid. index=%d size=%d", Index, Size);
424 }
425 }
426 }
427
428 return true;
429#undef Check
430 }
431};
432
433CDataFileReader::~CDataFileReader()
434{
435 Close();
436}
437
438CDataFileReader &CDataFileReader::operator=(CDataFileReader &&Other)
439{
440 m_pDataFile = Other.m_pDataFile;
441 Other.m_pDataFile = nullptr;
442 return *this;
443}
444
445bool CDataFileReader::Open(class IStorage *pStorage, const char *pFilename, int StorageType)
446{
447 dbg_assert(m_pDataFile == nullptr, "File already open");
448
449 log_trace("datafile", "loading '%s'", pFilename);
450
451 IOHANDLE File = pStorage->OpenFile(pFilename, Flags: IOFLAG_READ, Type: StorageType);
452 if(!File)
453 {
454 log_error("datafile", "failed to open file '%s' for reading", pFilename);
455 return false;
456 }
457
458 // determine size and hashes of the file and store them
459 int64_t FileSize = 0;
460 unsigned Crc = 0;
461 SHA256_DIGEST Sha256;
462 {
463 SHA256_CTX Sha256Ctxt;
464 sha256_init(ctxt: &Sha256Ctxt);
465 unsigned char aBuffer[64 * 1024];
466 while(true)
467 {
468 const unsigned Bytes = io_read(io: File, buffer: aBuffer, size: sizeof(aBuffer));
469 if(Bytes == 0)
470 break;
471 FileSize += Bytes;
472 Crc = crc32(crc: Crc, buf: aBuffer, len: Bytes);
473 sha256_update(ctxt: &Sha256Ctxt, data: aBuffer, data_len: Bytes);
474 }
475 Sha256 = sha256_finish(ctxt: &Sha256Ctxt);
476 if(io_seek(io: File, offset: 0, origin: IOSEEK_START) != 0)
477 {
478 io_close(io: File);
479 log_error("datafile", "could not seek to start after calculating hashes");
480 return false;
481 }
482 }
483
484 // read header
485 CDatafileHeader Header;
486 if(io_read(io: File, buffer: &Header, size: sizeof(Header)) != sizeof(Header))
487 {
488 io_close(io: File);
489 log_error("datafile", "could not read file header. file truncated or not a datafile.");
490 return false;
491 }
492
493 // check header magic
494 if((Header.m_aId[0] != 'A' || Header.m_aId[1] != 'T' || Header.m_aId[2] != 'A' || Header.m_aId[3] != 'D') &&
495 (Header.m_aId[0] != 'D' || Header.m_aId[1] != 'A' || Header.m_aId[2] != 'T' || Header.m_aId[3] != 'A'))
496 {
497 io_close(io: File);
498 log_error("datafile", "wrong header magic. magic=%x%x%x%x", Header.m_aId[0], Header.m_aId[1], Header.m_aId[2], Header.m_aId[3]);
499 return false;
500 }
501
502 SwapEndianInPlace(pObj: &Header);
503
504 // check header version
505 if(Header.m_Version != 3 && Header.m_Version != 4)
506 {
507 io_close(io: File);
508 log_error("datafile", "unsupported header version. version=%d", Header.m_Version);
509 return false;
510 }
511
512 // validate header information
513 if(Header.m_NumItemTypes < 0 ||
514 Header.m_NumItemTypes > MAX_ITEM_TYPE + 1 ||
515 Header.m_NumItems < 0 ||
516 Header.m_NumRawData < 0 ||
517 Header.m_ItemSize < 0 ||
518 Header.m_ItemSize % sizeof(int) != 0 ||
519 Header.m_DataSize < 0)
520 {
521 io_close(io: File);
522 log_error("datafile", "invalid header information. num_types=%d num_items=%d num_data=%d item_size=%d data_size=%d",
523 Header.m_NumItemTypes, Header.m_NumItems, Header.m_NumRawData, Header.m_ItemSize, Header.m_DataSize);
524 return false;
525 }
526
527 // calculate and validate sizes
528 int64_t Size = 0;
529 Size += (int64_t)Header.m_NumItemTypes * sizeof(CDatafileItemType);
530 Size += (int64_t)Header.m_NumItems * sizeof(int);
531 Size += (int64_t)Header.m_NumRawData * sizeof(int);
532 int64_t SizeFix = 0;
533 if(Header.m_Version == 4) // v4 has uncompressed data sizes as well
534 {
535 // The size of the uncompressed data sizes was not included in
536 // Header.m_Size and Header.m_Swaplen of version 4 maps prior
537 // to commit 3dd1ea0d8f6cb442ac41bd223279f41d1ed1b2bb. We also
538 // support loading maps created prior to this commit by fixing
539 // the sizes transparently when loading.
540 SizeFix = (int64_t)Header.m_NumRawData * sizeof(int);
541 Size += SizeFix;
542 }
543 Size += Header.m_ItemSize;
544
545 if((int64_t)sizeof(Header) + Size + (int64_t)Header.m_DataSize != FileSize)
546 {
547 io_close(io: File);
548 log_error("datafile", "invalid header data size or truncated file. data_size=%d file_size=%" PRId64, Header.m_DataSize, FileSize);
549 return false;
550 }
551
552 const int64_t HeaderFileSize = (int64_t)Header.m_Size + Header.SizeOffset();
553 if(HeaderFileSize != FileSize)
554 {
555 if(SizeFix != 0 && HeaderFileSize + SizeFix == FileSize)
556 {
557 log_warn("datafile", "fixing invalid header size. size=%d fix=+%" PRId64, Header.m_Size, SizeFix);
558 Header.m_Size += SizeFix;
559 }
560 else
561 {
562 io_close(io: File);
563 log_error("datafile", "invalid header size or truncated file. size=%" PRId64 " actual=%" PRId64, HeaderFileSize, FileSize);
564 return false;
565 }
566 }
567
568 const int64_t HeaderSwaplen = (int64_t)Header.m_Swaplen + Header.SizeOffset();
569 const int64_t FileSizeSwaplen = FileSize - Header.m_DataSize;
570 if(HeaderSwaplen != FileSizeSwaplen)
571 {
572 if(Header.m_Swaplen % sizeof(int) == 0 && SizeFix != 0 && HeaderSwaplen + SizeFix == FileSizeSwaplen)
573 {
574 log_warn("datafile", "fixing invalid header swaplen. swaplen=%d fix=+%" PRId64, Header.m_Swaplen, SizeFix);
575 Header.m_Swaplen += SizeFix;
576 }
577 else
578 {
579 io_close(io: File);
580 log_error("datafile", "invalid header swaplen or truncated file. swaplen=%" PRId64 " actual=%" PRId64, HeaderSwaplen, FileSizeSwaplen);
581 return false;
582 }
583 }
584
585 constexpr int64_t MaxAllocSize = (int64_t)2 * 1024 * 1024 * 1024;
586 int64_t AllocSize = Size;
587 AllocSize += sizeof(CDatafile); // add space for info structure
588 AllocSize += (int64_t)Header.m_NumRawData * sizeof(void *); // add space for data pointers
589 AllocSize += (int64_t)Header.m_NumRawData * sizeof(int); // add space for data sizes
590 if(AllocSize > MaxAllocSize)
591 {
592 io_close(io: File);
593 log_error("datafile", "file too large. alloc_size=%" PRId64 " max=%" PRId64, AllocSize, MaxAllocSize);
594 return false;
595 }
596
597 CDatafile *pTmpDataFile = static_cast<CDatafile *>(malloc(size: AllocSize));
598 if(pTmpDataFile == nullptr)
599 {
600 io_close(io: File);
601 log_error("datafile", "out of memory. could not allocate memory for datafile. alloc_size=%" PRId64, AllocSize);
602 return false;
603 }
604 pTmpDataFile->m_Header = Header;
605 pTmpDataFile->m_DataStartOffset = sizeof(CDatafileHeader) + Size;
606 pTmpDataFile->m_ppDataPtrs = (void **)(pTmpDataFile + 1);
607 pTmpDataFile->m_pDataSizes = (int *)(pTmpDataFile->m_ppDataPtrs + Header.m_NumRawData);
608 pTmpDataFile->m_pData = (char *)(pTmpDataFile->m_pDataSizes + Header.m_NumRawData);
609 pTmpDataFile->m_File = File;
610 pTmpDataFile->m_FileSize = FileSize;
611 pTmpDataFile->m_Sha256 = Sha256;
612 pTmpDataFile->m_Crc = Crc;
613
614 // clear the data pointers and sizes
615 mem_zero(block: pTmpDataFile->m_ppDataPtrs, size: Header.m_NumRawData * sizeof(void *));
616 mem_zero(block: pTmpDataFile->m_pDataSizes, size: Header.m_NumRawData * sizeof(int));
617
618 // read types, offsets, sizes and item data
619 const unsigned ReadSize = io_read(io: pTmpDataFile->m_File, buffer: pTmpDataFile->m_pData, size: Size);
620 if((int64_t)ReadSize != Size)
621 {
622 io_close(io: pTmpDataFile->m_File);
623 free(ptr: pTmpDataFile);
624 log_error("datafile", "truncation error. could not read all item data. wanted=%" PRId64 " got=%d", Size, ReadSize);
625 return false;
626 }
627
628 // The swap len also includes the size of the header (without the size offset), but the header was already swapped above.
629 const int64_t DataSwapLen = pTmpDataFile->m_Header.m_Swaplen - (int)(sizeof(Header) - Header.SizeOffset());
630 dbg_assert(DataSwapLen == Size, "Swap len and file size mismatch");
631 SwapEndianInPlace(pObj: pTmpDataFile->m_pData, Size: DataSwapLen);
632
633 pTmpDataFile->m_Info.m_pItemTypes = (CDatafileItemType *)pTmpDataFile->m_pData;
634 pTmpDataFile->m_Info.m_pItemOffsets = (int *)&pTmpDataFile->m_Info.m_pItemTypes[pTmpDataFile->m_Header.m_NumItemTypes];
635 pTmpDataFile->m_Info.m_pDataOffsets = &pTmpDataFile->m_Info.m_pItemOffsets[pTmpDataFile->m_Header.m_NumItems];
636 if(pTmpDataFile->m_Header.m_Version == 4) // v4 has uncompressed data sizes as well
637 {
638 pTmpDataFile->m_Info.m_pDataSizes = &pTmpDataFile->m_Info.m_pDataOffsets[pTmpDataFile->m_Header.m_NumRawData];
639 pTmpDataFile->m_Info.m_pItemStart = (char *)&pTmpDataFile->m_Info.m_pDataSizes[pTmpDataFile->m_Header.m_NumRawData];
640 }
641 else
642 {
643 pTmpDataFile->m_Info.m_pDataSizes = nullptr;
644 pTmpDataFile->m_Info.m_pItemStart = (char *)&pTmpDataFile->m_Info.m_pDataOffsets[pTmpDataFile->m_Header.m_NumRawData];
645 }
646 pTmpDataFile->m_Info.m_pDataStart = pTmpDataFile->m_Info.m_pItemStart + pTmpDataFile->m_Header.m_ItemSize;
647
648 if(!pTmpDataFile->Validate())
649 {
650 io_close(io: pTmpDataFile->m_File);
651 free(ptr: pTmpDataFile);
652 return false;
653 }
654
655 m_pDataFile = pTmpDataFile;
656 log_trace("datafile", "loading done. datafile='%s'", pFilename);
657
658 return true;
659}
660
661void CDataFileReader::Close()
662{
663 if(!m_pDataFile)
664 {
665 return;
666 }
667
668 for(int i = 0; i < m_pDataFile->m_Header.m_NumRawData; i++)
669 {
670 free(ptr: m_pDataFile->m_ppDataPtrs[i]);
671 }
672
673 io_close(io: m_pDataFile->m_File);
674 free(ptr: m_pDataFile);
675 m_pDataFile = nullptr;
676}
677
678bool CDataFileReader::IsOpen() const
679{
680 return m_pDataFile != nullptr;
681}
682
683IOHANDLE CDataFileReader::File() const
684{
685 dbg_assert(m_pDataFile != nullptr, "File not open");
686
687 return m_pDataFile->m_File;
688}
689
690int CDataFileReader::GetDataSize(int Index) const
691{
692 dbg_assert(m_pDataFile != nullptr, "File not open");
693
694 return m_pDataFile->GetDataSize(Index);
695}
696
697void *CDataFileReader::GetData(int Index)
698{
699 dbg_assert(m_pDataFile != nullptr, "File not open");
700
701 return m_pDataFile->GetData(Index, Swap: false);
702}
703
704void *CDataFileReader::GetDataSwapped(int Index)
705{
706 dbg_assert(m_pDataFile != nullptr, "File not open");
707
708 return m_pDataFile->GetData(Index, Swap: true);
709}
710
711const char *CDataFileReader::GetDataString(int Index)
712{
713 dbg_assert(m_pDataFile != nullptr, "File not open");
714
715 if(Index == -1)
716 {
717 return "";
718 }
719
720 const int DataSize = GetDataSize(Index);
721 if(!DataSize)
722 {
723 return nullptr;
724 }
725
726 const char *pData = static_cast<const char *>(GetData(Index));
727 if(pData == nullptr || mem_has_null(block: pData, size: DataSize - 1) || pData[DataSize - 1] != '\0' || !str_utf8_check(str: pData))
728 {
729 return nullptr;
730 }
731 return pData;
732}
733
734void CDataFileReader::ReplaceData(int Index, char *pData, size_t Size)
735{
736 dbg_assert(m_pDataFile != nullptr, "File not open");
737 dbg_assert(Index >= 0 && Index < m_pDataFile->m_Header.m_NumRawData, "Index invalid: %d", Index);
738
739 free(ptr: m_pDataFile->m_ppDataPtrs[Index]);
740 m_pDataFile->m_ppDataPtrs[Index] = pData;
741 m_pDataFile->m_pDataSizes[Index] = Size;
742}
743
744void CDataFileReader::UnloadData(int Index)
745{
746 dbg_assert(m_pDataFile != nullptr, "File not open");
747
748 if(Index < 0 || Index >= m_pDataFile->m_Header.m_NumRawData)
749 return;
750
751 free(ptr: m_pDataFile->m_ppDataPtrs[Index]);
752 m_pDataFile->m_ppDataPtrs[Index] = nullptr;
753 m_pDataFile->m_pDataSizes[Index] = 0;
754}
755
756int CDataFileReader::NumData() const
757{
758 dbg_assert(m_pDataFile != nullptr, "File not open");
759
760 return m_pDataFile->m_Header.m_NumRawData;
761}
762
763int CDataFileReader::GetItemSize(int Index) const
764{
765 dbg_assert(m_pDataFile != nullptr, "File not open");
766
767 return m_pDataFile->GetItemSize(Index);
768}
769
770int CDataFileReader::GetExternalItemType(int InternalType, CUuid *pUuid)
771{
772 if(InternalType <= OFFSET_UUID_TYPE || InternalType == ITEMTYPE_EX)
773 {
774 if(pUuid)
775 {
776 *pUuid = UUID_ZEROED;
777 }
778 return InternalType;
779 }
780
781 const int TypeIndex = FindItemIndex(Type: ITEMTYPE_EX, Id: InternalType);
782 if(TypeIndex < 0 || GetItemSize(Index: TypeIndex) < (int)sizeof(CItemEx))
783 {
784 if(pUuid)
785 {
786 *pUuid = UUID_ZEROED;
787 }
788 return InternalType;
789 }
790
791 const CItemEx *pItemEx = static_cast<const CItemEx *>(GetItem(Index: TypeIndex));
792 const CUuid Uuid = pItemEx->ToUuid();
793 if(pUuid)
794 {
795 *pUuid = Uuid;
796 }
797 // Propagate UUID_UNKNOWN, it doesn't hurt.
798 return g_UuidManager.LookupUuid(Uuid);
799}
800
801int CDataFileReader::GetInternalItemType(int ExternalType)
802{
803 if(ExternalType < OFFSET_UUID)
804 {
805 return ExternalType;
806 }
807
808 const CUuid Uuid = g_UuidManager.GetUuid(Id: ExternalType);
809 int Start, Num;
810 GetType(Type: ITEMTYPE_EX, pStart: &Start, pNum: &Num);
811 for(int Index = Start; Index < Start + Num; Index++)
812 {
813 if(GetItemSize(Index) < (int)sizeof(CItemEx))
814 {
815 continue;
816 }
817 int Id;
818 if(Uuid == static_cast<const CItemEx *>(GetItem(Index, pType: nullptr, pId: &Id))->ToUuid())
819 {
820 return Id;
821 }
822 }
823 return -1;
824}
825
826void *CDataFileReader::GetItem(int Index, int *pType, int *pId, CUuid *pUuid)
827{
828 dbg_assert(m_pDataFile != nullptr, "File not open");
829
830 CDatafileItem *pItem = m_pDataFile->GetItem(Index);
831 const int ExternalType = GetExternalItemType(InternalType: pItem->Type(), pUuid);
832 if(pType)
833 {
834 *pType = ExternalType;
835 }
836 if(pId)
837 {
838 *pId = pItem->Id();
839 }
840 return static_cast<void *>(pItem + 1);
841}
842
843void CDataFileReader::GetType(int Type, int *pStart, int *pNum)
844{
845 dbg_assert(m_pDataFile != nullptr, "File not open");
846
847 *pStart = 0;
848 *pNum = 0;
849
850 const int InternalType = GetInternalItemType(ExternalType: Type);
851 for(int Index = 0; Index < m_pDataFile->m_Header.m_NumItemTypes; Index++)
852 {
853 const CDatafileItemType &ItemType = m_pDataFile->m_Info.m_pItemTypes[Index];
854 if(ItemType.m_Type == InternalType)
855 {
856 *pStart = ItemType.m_Start;
857 *pNum = ItemType.m_Num;
858 return;
859 }
860 }
861}
862
863int CDataFileReader::FindItemIndex(int Type, int Id)
864{
865 dbg_assert(m_pDataFile != nullptr, "File not open");
866
867 int Start, Num;
868 GetType(Type, pStart: &Start, pNum: &Num);
869 for(int Index = Start; Index < Start + Num; Index++)
870 {
871 const CDatafileItem *pItem = m_pDataFile->GetItem(Index);
872 if(pItem->Id() == Id)
873 {
874 return Index;
875 }
876 }
877 return -1;
878}
879
880void *CDataFileReader::FindItem(int Type, int Id)
881{
882 const int Index = FindItemIndex(Type, Id);
883 if(Index < 0)
884 {
885 return nullptr;
886 }
887 return GetItem(Index);
888}
889
890int CDataFileReader::NumItems() const
891{
892 dbg_assert(m_pDataFile != nullptr, "File not open");
893
894 return m_pDataFile->m_Header.m_NumItems;
895}
896
897SHA256_DIGEST CDataFileReader::Sha256() const
898{
899 dbg_assert(m_pDataFile != nullptr, "File not open");
900
901 return m_pDataFile->m_Sha256;
902}
903
904unsigned CDataFileReader::Crc() const
905{
906 dbg_assert(m_pDataFile != nullptr, "File not open");
907
908 return m_pDataFile->m_Crc;
909}
910
911int CDataFileReader::MapSize() const
912{
913 dbg_assert(m_pDataFile != nullptr, "File not open");
914
915 return m_pDataFile->m_FileSize;
916}
917
918CDataFileWriter::CDataFileWriter()
919{
920 m_File = nullptr;
921}
922
923CDataFileWriter::~CDataFileWriter()
924{
925 if(m_File)
926 {
927 io_close(io: m_File);
928 m_File = nullptr;
929 }
930
931 for(CItemInfo &ItemInfo : m_vItems)
932 {
933 free(ptr: ItemInfo.m_pData);
934 }
935
936 for(CDataInfo &DataInfo : m_vDatas)
937 {
938 free(ptr: DataInfo.m_pUncompressedData);
939 free(ptr: DataInfo.m_pCompressedData);
940 }
941}
942
943bool CDataFileWriter::Open(class IStorage *pStorage, const char *pFilename, int StorageType)
944{
945 dbg_assert(!m_File, "File already open");
946 m_File = pStorage->OpenFile(pFilename, Flags: IOFLAG_WRITE, Type: StorageType);
947 return m_File != nullptr;
948}
949
950int CDataFileWriter::GetTypeFromIndex(int Index) const
951{
952 return ITEMTYPE_EX - Index - 1;
953}
954
955int CDataFileWriter::GetExtendedItemTypeIndex(int Type, const CUuid *pUuid)
956{
957 int Index = 0;
958 if(Type == -1)
959 {
960 // Unknown type, search for UUID
961 for(const auto &ExtendedItemType : m_vExtendedItemTypes)
962 {
963 if(ExtendedItemType.m_Uuid == *pUuid)
964 {
965 return Index;
966 }
967 ++Index;
968 }
969 }
970 else
971 {
972 for(const auto &ExtendedItemType : m_vExtendedItemTypes)
973 {
974 if(ExtendedItemType.m_Type == Type)
975 {
976 return Index;
977 }
978 ++Index;
979 }
980 }
981
982 // Type not found, add it.
983 const CUuid Uuid = Type == -1 ? *pUuid : g_UuidManager.GetUuid(Id: Type);
984 CExtendedItemType ExtendedType;
985 ExtendedType.m_Type = Type;
986 ExtendedType.m_Uuid = Uuid;
987 m_vExtendedItemTypes.emplace_back(args&: ExtendedType);
988
989 const CItemEx ItemEx = CItemEx::FromUuid(Uuid);
990 AddItem(Type: ITEMTYPE_EX, Id: GetTypeFromIndex(Index), Size: sizeof(ItemEx), pData: &ItemEx);
991 return Index;
992}
993
994int CDataFileWriter::AddItem(int Type, int Id, size_t Size, const void *pData, const CUuid *pUuid)
995{
996 dbg_assert((Type >= 0 && Type <= MAX_ITEM_TYPE) || Type >= OFFSET_UUID || (Type == -1 && pUuid != nullptr), "Invalid Type: %d", Type);
997 dbg_assert(Id >= 0 && Id <= MAX_ITEM_ID, "Invalid Id: %d", Id);
998 dbg_assert(Size == 0 || pData != nullptr, "Data missing"); // Items without data are allowed
999 dbg_assert(Size <= (size_t)std::numeric_limits<int>::max(), "Data too large");
1000 dbg_assert(Size % sizeof(int) == 0, "Invalid data boundary");
1001 dbg_assert(m_vItems.size() < (size_t)std::numeric_limits<int>::max(), "Too many items");
1002
1003 if(Type == -1 || Type >= OFFSET_UUID)
1004 {
1005 Type = GetTypeFromIndex(Index: GetExtendedItemTypeIndex(Type, pUuid));
1006 }
1007
1008 const int NumItems = m_vItems.size();
1009 m_vItems.emplace_back();
1010 CItemInfo &Info = m_vItems.back();
1011 Info.m_Type = Type;
1012 Info.m_Id = Id;
1013 Info.m_Size = Size;
1014
1015 // copy data
1016 if(Size > 0)
1017 {
1018 Info.m_pData = malloc(size: Size);
1019 mem_copy(dest: Info.m_pData, source: pData, size: Size);
1020 }
1021 else
1022 {
1023 Info.m_pData = nullptr;
1024 }
1025
1026 // link
1027 CItemTypeInfo &ItemType = m_ItemTypes[Type];
1028 Info.m_Prev = ItemType.m_Last;
1029 Info.m_Next = -1;
1030
1031 if(ItemType.m_Last != -1)
1032 {
1033 m_vItems[ItemType.m_Last].m_Next = NumItems;
1034 }
1035 ItemType.m_Last = NumItems;
1036
1037 if(ItemType.m_First == -1)
1038 {
1039 ItemType.m_First = NumItems;
1040 }
1041
1042 ItemType.m_Num++;
1043 return NumItems;
1044}
1045
1046int CDataFileWriter::AddData(size_t Size, const void *pData, ECompressionLevel CompressionLevel)
1047{
1048 dbg_assert(Size > 0 && pData != nullptr, "Data missing");
1049 dbg_assert(Size <= (size_t)std::numeric_limits<int>::max(), "Data too large");
1050 dbg_assert(m_vDatas.size() < (size_t)std::numeric_limits<int>::max(), "Too many data");
1051
1052 CDataInfo Info;
1053 Info.m_pUncompressedData = malloc(size: Size);
1054 mem_copy(dest: Info.m_pUncompressedData, source: pData, size: Size);
1055 Info.m_UncompressedSize = Size;
1056 Info.m_pCompressedData = nullptr;
1057 Info.m_CompressedSize = 0;
1058 Info.m_CompressionLevel = CompressionLevel;
1059 m_vDatas.emplace_back(args&: Info);
1060
1061 return m_vDatas.size() - 1;
1062}
1063
1064int CDataFileWriter::AddDataSwapped(size_t Size, const void *pData)
1065{
1066 dbg_assert(Size > 0 && pData != nullptr, "Data missing");
1067 dbg_assert(Size <= (size_t)std::numeric_limits<int>::max(), "Data too large");
1068 dbg_assert(m_vDatas.size() < (size_t)std::numeric_limits<int>::max(), "Too many data");
1069 dbg_assert(Size % sizeof(int) == 0, "Invalid data boundary");
1070
1071#if defined(CONF_ARCH_ENDIAN_BIG)
1072 void *pSwapped = malloc(Size); // temporary buffer that we use during compression
1073 mem_copy(pSwapped, pData, Size);
1074 swap_endian(pSwapped, sizeof(int), Size / sizeof(int));
1075 int Index = AddData(Size, pSwapped);
1076 free(pSwapped);
1077 return Index;
1078#else
1079 return AddData(Size, pData);
1080#endif
1081}
1082
1083int CDataFileWriter::AddDataString(const char *pStr)
1084{
1085 dbg_assert(pStr != nullptr, "Data missing");
1086
1087 if(pStr[0] == '\0')
1088 {
1089 return -1;
1090 }
1091 return AddData(Size: str_length(str: pStr) + 1, pData: pStr);
1092}
1093
1094static int CompressionLevelToZlib(CDataFileWriter::ECompressionLevel CompressionLevel)
1095{
1096 switch(CompressionLevel)
1097 {
1098 case CDataFileWriter::COMPRESSION_DEFAULT:
1099 return Z_DEFAULT_COMPRESSION;
1100 case CDataFileWriter::COMPRESSION_BEST:
1101 return Z_BEST_COMPRESSION;
1102 default:
1103 dbg_assert_failed("Invalid CompressionLevel: %d", static_cast<int>(CompressionLevel));
1104 }
1105}
1106
1107void CDataFileWriter::Finish()
1108{
1109 dbg_assert((bool)m_File, "File not open");
1110
1111 // Compress data. This takes the majority of the time when saving a datafile,
1112 // so it's delayed until the end so it can be off-loaded to another thread.
1113 for(CDataInfo &DataInfo : m_vDatas)
1114 {
1115 unsigned long CompressedSize = compressBound(sourceLen: DataInfo.m_UncompressedSize);
1116 DataInfo.m_pCompressedData = malloc(size: CompressedSize);
1117 const int Result = compress2(dest: static_cast<Bytef *>(DataInfo.m_pCompressedData), destLen: &CompressedSize, source: static_cast<Bytef *>(DataInfo.m_pUncompressedData), sourceLen: DataInfo.m_UncompressedSize, level: CompressionLevelToZlib(CompressionLevel: DataInfo.m_CompressionLevel));
1118 DataInfo.m_CompressedSize = CompressedSize;
1119 free(ptr: DataInfo.m_pUncompressedData);
1120 DataInfo.m_pUncompressedData = nullptr;
1121 dbg_assert(Result == Z_OK, "datafile zlib compression failed with error %d", Result);
1122 }
1123
1124 // Calculate total size of items
1125 int64_t ItemSize = 0;
1126 for(const CItemInfo &ItemInfo : m_vItems)
1127 {
1128 ItemSize += ItemInfo.m_Size;
1129 ItemSize += sizeof(CDatafileItem);
1130 }
1131
1132 // Calculate total size of data
1133 int64_t DataSize = 0;
1134 for(const CDataInfo &DataInfo : m_vDatas)
1135 {
1136 DataSize += DataInfo.m_CompressedSize;
1137 }
1138
1139 // Calculate complete file size
1140 const int64_t TypesSize = m_ItemTypes.size() * sizeof(CDatafileItemType);
1141 const int64_t HeaderSize = sizeof(CDatafileHeader);
1142 const int64_t OffsetSize = (m_vItems.size() + m_vDatas.size() * 2) * sizeof(int); // ItemOffsets, DataOffsets, DataUncompressedSizes
1143 const int64_t SwapSize = HeaderSize + TypesSize + OffsetSize + ItemSize;
1144 const int64_t FileSize = SwapSize + DataSize;
1145
1146 // This also ensures that SwapSize, ItemSize and DataSize are valid.
1147 dbg_assert(FileSize <= (int64_t)std::numeric_limits<int>::max(), "File size too large");
1148
1149 // Construct and write header
1150 {
1151 CDatafileHeader Header;
1152 Header.m_aId[0] = 'D';
1153 Header.m_aId[1] = 'A';
1154 Header.m_aId[2] = 'T';
1155 Header.m_aId[3] = 'A';
1156 Header.m_Version = 4;
1157 Header.m_Size = FileSize - Header.SizeOffset();
1158 Header.m_Swaplen = SwapSize - Header.SizeOffset();
1159 Header.m_NumItemTypes = m_ItemTypes.size();
1160 Header.m_NumItems = m_vItems.size();
1161 Header.m_NumRawData = m_vDatas.size();
1162 Header.m_ItemSize = ItemSize;
1163 Header.m_DataSize = DataSize;
1164
1165 SwapEndianInPlace(pObj: &Header);
1166 io_write(io: m_File, buffer: &Header, size: sizeof(Header));
1167 }
1168
1169 // Write item types
1170 int ItemCount = 0;
1171 for(const auto &[Type, ItemType] : m_ItemTypes)
1172 {
1173 dbg_assert(ItemType.m_Num > 0, "Invalid ItemType.m_Num: %d", ItemType.m_Num);
1174
1175 CDatafileItemType Info;
1176 Info.m_Type = Type;
1177 Info.m_Start = ItemCount;
1178 Info.m_Num = ItemType.m_Num;
1179
1180 SwapEndianInPlace(pObj: &Info);
1181 io_write(io: m_File, buffer: &Info, size: sizeof(Info));
1182 ItemCount += ItemType.m_Num;
1183 }
1184
1185 // Write item offsets sorted by type
1186 int ItemOffset = 0;
1187 for(const auto &[Type, ItemType] : m_ItemTypes)
1188 {
1189 // Write all items offsets of this type
1190 for(int ItemIndex = ItemType.m_First; ItemIndex != -1; ItemIndex = m_vItems[ItemIndex].m_Next)
1191 {
1192 const int ItemOffsetWrite = SwapEndianInt(Number: ItemOffset);
1193 io_write(io: m_File, buffer: &ItemOffsetWrite, size: sizeof(ItemOffsetWrite));
1194 ItemOffset += m_vItems[ItemIndex].m_Size + sizeof(CDatafileItem);
1195 }
1196 }
1197
1198 // Write data offsets
1199 int DataOffset = 0;
1200 for(const CDataInfo &DataInfo : m_vDatas)
1201 {
1202 const int DataOffsetWrite = SwapEndianInt(Number: DataOffset);
1203 io_write(io: m_File, buffer: &DataOffsetWrite, size: sizeof(DataOffsetWrite));
1204 DataOffset += DataInfo.m_CompressedSize;
1205 }
1206
1207 // Write data uncompressed sizes
1208 for(const CDataInfo &DataInfo : m_vDatas)
1209 {
1210 const int UncompressedSizeWrite = SwapEndianInt(Number: DataInfo.m_UncompressedSize);
1211 io_write(io: m_File, buffer: &UncompressedSizeWrite, size: sizeof(UncompressedSizeWrite));
1212 }
1213
1214 // Write items sorted by type
1215 for(const auto &[Type, ItemType] : m_ItemTypes)
1216 {
1217 // Write all items of this type
1218 for(int ItemIndex = ItemType.m_First; ItemIndex != -1; ItemIndex = m_vItems[ItemIndex].m_Next)
1219 {
1220 CDatafileItem Item;
1221 Item.m_TypeAndId = ((unsigned)Type << 16u) | (unsigned)m_vItems[ItemIndex].m_Id;
1222 Item.m_Size = m_vItems[ItemIndex].m_Size;
1223
1224 SwapEndianInPlace(pObj: &Item);
1225 io_write(io: m_File, buffer: &Item, size: sizeof(Item));
1226
1227 if(m_vItems[ItemIndex].m_pData != nullptr)
1228 {
1229 SwapEndianInPlace(pObj: m_vItems[ItemIndex].m_pData, Size: m_vItems[ItemIndex].m_Size);
1230 io_write(io: m_File, buffer: m_vItems[ItemIndex].m_pData, size: m_vItems[ItemIndex].m_Size);
1231 free(ptr: m_vItems[ItemIndex].m_pData);
1232 m_vItems[ItemIndex].m_pData = nullptr;
1233 }
1234 }
1235 }
1236
1237 // Write data
1238 for(CDataInfo &DataInfo : m_vDatas)
1239 {
1240 io_write(io: m_File, buffer: DataInfo.m_pCompressedData, size: DataInfo.m_CompressedSize);
1241 free(ptr: DataInfo.m_pCompressedData);
1242 DataInfo.m_pCompressedData = nullptr;
1243 }
1244
1245 io_close(io: m_File);
1246 m_File = nullptr;
1247}
1248