| 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 | |
| 21 | static constexpr int MAX_ITEM_TYPE = 0xFFFF; |
| 22 | static constexpr int MAX_ITEM_ID = 0xFFFF; |
| 23 | static constexpr int OFFSET_UUID_TYPE = 0x8000; |
| 24 | |
| 25 | static 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 | |
| 32 | template<typename T> |
| 33 | static inline void SwapEndianInPlace(T *pObj) |
| 34 | { |
| 35 | static_assert(sizeof(T) % sizeof(int) == 0); |
| 36 | SwapEndianInPlace(pObj, sizeof(T)); |
| 37 | } |
| 38 | |
| 39 | static inline int SwapEndianInt(int Number) |
| 40 | { |
| 41 | SwapEndianInPlace(pObj: &Number); |
| 42 | return Number; |
| 43 | } |
| 44 | |
| 45 | class CItemEx |
| 46 | { |
| 47 | public: |
| 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 | |
| 71 | class CDatafileItemType |
| 72 | { |
| 73 | public: |
| 74 | int m_Type; |
| 75 | int m_Start; |
| 76 | int m_Num; |
| 77 | }; |
| 78 | |
| 79 | class CDatafileItem |
| 80 | { |
| 81 | public: |
| 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 | |
| 96 | class |
| 97 | { |
| 98 | public: |
| 99 | char [4]; |
| 100 | int ; |
| 101 | int ; |
| 102 | int ; |
| 103 | int ; |
| 104 | int ; |
| 105 | int ; |
| 106 | int ; |
| 107 | int ; |
| 108 | |
| 109 | constexpr size_t () |
| 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 | |
| 116 | class CDatafileInfo |
| 117 | { |
| 118 | public: |
| 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 | |
| 128 | class CDatafile |
| 129 | { |
| 130 | public: |
| 131 | IOHANDLE m_File; |
| 132 | unsigned m_FileSize; |
| 133 | SHA256_DIGEST m_Sha256; |
| 134 | unsigned m_Crc; |
| 135 | CDatafileInfo m_Info; |
| 136 | CDatafileHeader ; |
| 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 | |
| 433 | CDataFileReader::~CDataFileReader() |
| 434 | { |
| 435 | Close(); |
| 436 | } |
| 437 | |
| 438 | CDataFileReader &CDataFileReader::operator=(CDataFileReader &&Other) |
| 439 | { |
| 440 | m_pDataFile = Other.m_pDataFile; |
| 441 | Other.m_pDataFile = nullptr; |
| 442 | return *this; |
| 443 | } |
| 444 | |
| 445 | bool 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 ; |
| 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 = (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 = (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 | |
| 661 | void 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 | |
| 678 | bool CDataFileReader::IsOpen() const |
| 679 | { |
| 680 | return m_pDataFile != nullptr; |
| 681 | } |
| 682 | |
| 683 | IOHANDLE CDataFileReader::File() const |
| 684 | { |
| 685 | dbg_assert(m_pDataFile != nullptr, "File not open" ); |
| 686 | |
| 687 | return m_pDataFile->m_File; |
| 688 | } |
| 689 | |
| 690 | int CDataFileReader::GetDataSize(int Index) const |
| 691 | { |
| 692 | dbg_assert(m_pDataFile != nullptr, "File not open" ); |
| 693 | |
| 694 | return m_pDataFile->GetDataSize(Index); |
| 695 | } |
| 696 | |
| 697 | void *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 | |
| 704 | void *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 | |
| 711 | const 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 | |
| 734 | void 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 | |
| 744 | void 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 | |
| 756 | int CDataFileReader::NumData() const |
| 757 | { |
| 758 | dbg_assert(m_pDataFile != nullptr, "File not open" ); |
| 759 | |
| 760 | return m_pDataFile->m_Header.m_NumRawData; |
| 761 | } |
| 762 | |
| 763 | int CDataFileReader::GetItemSize(int Index) const |
| 764 | { |
| 765 | dbg_assert(m_pDataFile != nullptr, "File not open" ); |
| 766 | |
| 767 | return m_pDataFile->GetItemSize(Index); |
| 768 | } |
| 769 | |
| 770 | int 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 | |
| 801 | int 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 | |
| 826 | void *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 | |
| 843 | void 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 | |
| 863 | int 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 | |
| 880 | void *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 | |
| 890 | int CDataFileReader::NumItems() const |
| 891 | { |
| 892 | dbg_assert(m_pDataFile != nullptr, "File not open" ); |
| 893 | |
| 894 | return m_pDataFile->m_Header.m_NumItems; |
| 895 | } |
| 896 | |
| 897 | SHA256_DIGEST CDataFileReader::Sha256() const |
| 898 | { |
| 899 | dbg_assert(m_pDataFile != nullptr, "File not open" ); |
| 900 | |
| 901 | return m_pDataFile->m_Sha256; |
| 902 | } |
| 903 | |
| 904 | unsigned CDataFileReader::Crc() const |
| 905 | { |
| 906 | dbg_assert(m_pDataFile != nullptr, "File not open" ); |
| 907 | |
| 908 | return m_pDataFile->m_Crc; |
| 909 | } |
| 910 | |
| 911 | int CDataFileReader::MapSize() const |
| 912 | { |
| 913 | dbg_assert(m_pDataFile != nullptr, "File not open" ); |
| 914 | |
| 915 | return m_pDataFile->m_FileSize; |
| 916 | } |
| 917 | |
| 918 | CDataFileWriter::CDataFileWriter() |
| 919 | { |
| 920 | m_File = nullptr; |
| 921 | } |
| 922 | |
| 923 | CDataFileWriter::~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 | |
| 943 | bool 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 | |
| 950 | int CDataFileWriter::GetTypeFromIndex(int Index) const |
| 951 | { |
| 952 | return ITEMTYPE_EX - Index - 1; |
| 953 | } |
| 954 | |
| 955 | int 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 | |
| 994 | int 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 | |
| 1046 | int 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 | |
| 1064 | int 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 | |
| 1083 | int 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 | |
| 1094 | static 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 | |
| 1107 | void 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 = 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 ; |
| 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 | |