Monopoly — Portage moderne 0.1.0
Portage moderne C/C++23 du Monopoly de 1999
Loading...
Searching...
No Matches
LegacyDataArchive.cpp
Go to the documentation of this file.
2
3#include <zlib.h>
4
5#include <algorithm>
6#include <array>
7#include <bit>
8#include <cstring>
9#include <limits>
10#include <list>
11#include <sstream>
12#include <utility>
13
14namespace monopoly::data
15{
16 namespace detail
17 {
18 class RawDataPool;
19
20 // The public shared pointer aliases this lease. Its final release is
21 // observable, unlike polling use_count(), so release-to-zero refreshes
22 // recency exactly as L_Data's reference-counted unused-item LRU does.
24 {
26 std::shared_ptr<RawDataPool> pool;
27 std::uint64_t identity{};
29 };
30
31 class RawDataPool final : public std::enable_shared_from_this<RawDataPool>
32 {
33 struct Entry
34 {
36 std::weak_ptr<const DataBytes> lease;
37 bool cached{true};
38 std::uint64_t identity{};
39 };
40
41 using Entries = std::list<Entry>;
42
43 public:
44 SharedDataBytes acquire(const std::weak_ptr<const DataBytes>& cached)
45 {
46 const std::scoped_lock lock(mutex_);
47 const auto data = cached.lock();
48 if (!data) return {};
49 const auto item = find(data.get());
50 if (item == entries_.end() || !item->cached) return {};
51 entries_.splice(entries_.begin(), entries_, item);
52 if (auto lease = item->lease.lock()) return lease;
53 return makeLease(*item);
54 }
55
56 void reserve(std::size_t size)
57 {
58 const std::scoped_lock lock(mutex_);
59 // L_Data ignores failure to free enough space and attempts
60 // allocation anyway. Leases and individual large valid items
61 // may exceed this soft threshold; neither is invalidated.
62 auto item = entries_.end();
63 while (residentBytes_ + reservedBytes_ + size > LegacyRawDataCacheBudget &&
64 item != entries_.begin())
65 {
66 --item;
67 if (!item->lease.expired()) continue;
68 residentBytes_ -= item->data->size();
69 item = entries_.erase(item);
70 }
71 reservedBytes_ += size;
72 }
73
74 void cancelReservation(std::size_t size) noexcept
75 {
76 const std::scoped_lock lock(mutex_);
77 reservedBytes_ -= size;
78 }
79
80 SharedDataBytes publish(SharedDataBytes data, std::size_t reserved)
81 {
82 const std::scoped_lock lock(mutex_);
83 entries_.push_front({std::move(data), {}, true, ++lastIdentity_});
84 SharedDataBytes lease;
85 try { lease = makeLease(entries_.front()); }
86 catch (...) { entries_.pop_front(); throw; }
87 residentBytes_ += entries_.front().data->size();
88 reservedBytes_ -= reserved;
89 return lease;
90 }
91
92 bool remove(const std::weak_ptr<const DataBytes>& cached) noexcept
93 {
94 const std::scoped_lock lock(mutex_);
95 const auto data = cached.lock();
96 if (!data) return false;
97 const auto item = find(data.get());
98 if (item == entries_.end() || !item->cached) return false;
99 item->cached = false;
100 if (item->lease.expired()) erase(item);
101 return true;
102 }
103
104 void released(std::uint64_t identity) noexcept
105 {
106 const std::scoped_lock lock(mutex_);
107 const auto item = std::find_if(entries_.begin(), entries_.end(),
108 [identity](const Entry& entry) { return entry.identity == identity; });
109 if (item == entries_.end()) return;
110 // A concurrent cache hit can already have published a new
111 // lease while the old lease destructor awaited this mutex.
112 if (!item->lease.expired()) return;
113 if (!item->cached) erase(item);
114 else entries_.splice(entries_.begin(), entries_, item);
115 }
116
118 {
119 const std::scoped_lock lock(mutex_);
120 RawDataCacheStats result{residentBytes_, reservedBytes_};
121 for (const auto& item : entries_)
122 {
123 result.cachedItems += item.cached;
124 result.leasedItems += !item.lease.expired();
125 }
126 return result;
127 }
128
129 private:
130 Entries::iterator find(const DataBytes* data) noexcept
131 {
132 return std::find_if(entries_.begin(), entries_.end(),
133 [data](const Entry& item) { return item.data.get() == data; });
134 }
135
136 SharedDataBytes makeLease(Entry& item)
137 {
138 auto holder = std::make_shared<RawDataLease>();
139 holder->data = item.data;
140 holder->pool = shared_from_this();
141 holder->identity = item.identity;
142 SharedDataBytes lease(holder, item.data.get());
143 item.lease = lease;
144 return lease;
145 }
146
147 void erase(Entries::iterator item) noexcept
148 {
149 residentBytes_ -= item->data->size();
150 entries_.erase(item);
151 }
152
153 mutable std::mutex mutex_;
154 Entries entries_; // Most recently accessed/released at the front.
155 std::size_t residentBytes_{};
156 std::size_t reservedBytes_{};
157 std::uint64_t lastIdentity_{};
158 };
159
161 {
162 data.reset();
163 if (pool) pool->released(identity);
164 }
165
166 const std::shared_ptr<RawDataPool>& rawDataPool()
167 {
168 // Leases retain the pool through static shutdown as well. The
169 // pool never holds a strong lease, so this does not form a cycle.
170 static const auto pool = std::make_shared<RawDataPool>();
171 return pool;
172 }
173
175 {
176 public:
177 RawDataReservation(std::shared_ptr<RawDataPool> pool, std::size_t size)
178 : pool_(std::move(pool)), size_(size) { pool_->reserve(size_); }
179 ~RawDataReservation() { if (size_) pool_->cancelReservation(size_); }
183 {
184 auto result = pool_->publish(std::move(data), size_);
185 size_ = 0;
186 return result;
187 }
188 private:
189 std::shared_ptr<RawDataPool> pool_;
190 std::size_t size_{};
191 };
192
193 }
194
195 namespace
196 {
197 constexpr std::array<char, 7> ArtechSignature
198 {{ 'A', 'r', 't', 'e', 'c', 'h', '\0' }};
199
200
201 [[nodiscard]] std::uint16_t readU16Le(
202 const std::byte* bytes) noexcept
203 {
204 return
205 static_cast<std::uint16_t>(
206 std::to_integer<std::uint8_t>(bytes[0])) |
207 static_cast<std::uint16_t>(
208 std::to_integer<std::uint8_t>(bytes[1]) << 8U);
209 }
210
211
212 [[nodiscard]] std::uint32_t readU32Le(
213 const std::byte* bytes) noexcept
214 {
215 return
216 static_cast<std::uint32_t>(
217 std::to_integer<std::uint8_t>(bytes[0])) |
218 (static_cast<std::uint32_t>(
219 std::to_integer<std::uint8_t>(bytes[1])) << 8U) |
220 (static_cast<std::uint32_t>(
221 std::to_integer<std::uint8_t>(bytes[2])) << 16U) |
222 (static_cast<std::uint32_t>(
223 std::to_integer<std::uint8_t>(bytes[3])) << 24U);
224 }
225
226
227 [[nodiscard]] bool readExact(
228 std::ifstream& stream,
229 std::span<std::byte> destination)
230 {
231 if (destination.empty())
232 {
233 return true;
234 }
235
236 stream.read(
237 reinterpret_cast<char*>(destination.data()),
238 static_cast<std::streamsize>(destination.size()));
239
240 return
241 stream.gcount() ==
242 static_cast<std::streamsize>(destination.size());
243 }
244
245
246 [[nodiscard]] std::string itemDetail(
247 std::string_view prefix,
248 std::size_t item)
249 {
250 std::ostringstream output;
251 output << prefix << " (item " << item << ')';
252 return output.str();
253 }
254 }
255
260
261 std::string_view legacyDataTypeName(LegacyDataType type) noexcept
262 {
263 switch (type)
264 {
265 case LegacyDataType::Unknown: return "Unknown";
266 case LegacyDataType::Bitmap: return "BMP";
267 case LegacyDataType::DopeTable: return "DopeTAB";
268 case LegacyDataType::Uap: return "UAP";
269 case LegacyDataType::Native: return "Native";
270 case LegacyDataType::GbmTexture: return "GBMTexture";
271 case LegacyDataType::GbmPicture: return "GBMPicture";
272 case LegacyDataType::GenericBitmap: return "GenericBitmap";
273 case LegacyDataType::Wave: return "Wave";
274 case LegacyDataType::String: return "String";
275 case LegacyDataType::UserCreated1: return "UserCreated1";
276 case LegacyDataType::IndexTable: return "IndexTable";
277 case LegacyDataType::Chunky: return "Chunky";
278 case LegacyDataType::TextureArray: return "TextureArray";
279 case LegacyDataType::Model3D: return "3DModel";
280 case LegacyDataType::Pose3D: return "3DPose";
281 case LegacyDataType::Hmd: return "HMD";
282 case LegacyDataType::MeshX: return "MESHX";
283 }
284
285 return "Invalid";
286 }
287
288
289 std::string_view dataErrorCodeName(DataErrorCode code) noexcept
290 {
291 switch (code)
292 {
293 case DataErrorCode::None: return "None";
294 case DataErrorCode::InvalidGroup: return "InvalidGroup";
295 case DataErrorCode::InvalidLanguage: return "InvalidLanguage";
296 case DataErrorCode::NoActiveLanguage: return "NoActiveLanguage";
297 case DataErrorCode::DuplicateGroup: return "DuplicateGroup";
298 case DataErrorCode::GroupNotMounted: return "GroupNotMounted";
299 case DataErrorCode::ArchiveClosed: return "ArchiveClosed";
300 case DataErrorCode::FileOpenFailed: return "FileOpenFailed";
301 case DataErrorCode::FileSizeFailed: return "FileSizeFailed";
302 case DataErrorCode::HeaderTruncated: return "HeaderTruncated";
303 case DataErrorCode::InvalidSignature: return "InvalidSignature";
304 case DataErrorCode::UnsupportedVersion: return "UnsupportedVersion";
305 case DataErrorCode::InvalidHeader: return "InvalidHeader";
306 case DataErrorCode::IndexTooLarge: return "IndexTooLarge";
307 case DataErrorCode::IndexTruncated: return "IndexTruncated";
308 case DataErrorCode::InvalidItemType: return "InvalidItemType";
309 case DataErrorCode::InvalidItemRange: return "InvalidItemRange";
310 case DataErrorCode::ChecksumMismatch: return "ChecksumMismatch";
311 case DataErrorCode::TagOutOfRange: return "TagOutOfRange";
312 case DataErrorCode::EmptyItem: return "EmptyItem";
313 case DataErrorCode::ReadFailed: return "ReadFailed";
314 case DataErrorCode::DecompressionFailed: return "DecompressionFailed";
316 return "DecompressedSizeMismatch";
317 case DataErrorCode::ArchiveBuildFailed: return "ArchiveBuildFailed";
318 case DataErrorCode::WriteFailed: return "WriteFailed";
319 case DataErrorCode::InvalidIndexTable: return "InvalidIndexTable";
321 return "UnsortedIndexTable";
322 case DataErrorCode::DuplicateIndexKey: return "DuplicateIndexKey";
323 case DataErrorCode::DuplicateDataId: return "DuplicateDataId";
325 return "IndexedItemNotFound";
326 case DataErrorCode::TypeMismatch: return "TypeMismatch";
327 case DataErrorCode::InvalidUtf16: return "InvalidUtf16";
328 case DataErrorCode::InvalidBoardEdition: return "InvalidBoardEdition";
329 case DataErrorCode::ResourcePathInvalid: return "ResourcePathInvalid";
330 case DataErrorCode::ResourceNotFound: return "ResourceNotFound";
331 case DataErrorCode::ResourcePathAmbiguous: return "ResourcePathAmbiguous";
332 case DataErrorCode::ResourcePathFailed: return "ResourcePathFailed";
333 }
334
335 return "InvalidDataErrorCode";
336 }
337
338
339 DataError LegacyDataArchive::makeError(
340 DataErrorCode code,
341 const std::filesystem::path& path,
342 std::string detail,
343 std::optional<DataTag> tag)
344 {
345 return { code, path, tag, std::move(detail) };
346 }
347
348
353
354
355 std::expected<std::shared_ptr<LegacyDataArchive>, DataError>
357 const std::filesystem::path& path,
358 std::uint16_t group,
359 ArchiveOpenOptions options)
360 {
361 if (group == 0)
362 {
363 return std::unexpected(makeError(
365 path,
366 "legacy group zero is reserved for EmptyDataId"));
367 }
368
369 auto archive = std::shared_ptr<LegacyDataArchive>(
370 new LegacyDataArchive());
371 archive->rawDataPool_ = detail::rawDataPool();
372
373 archive->path_ = path;
374 archive->group_ = group;
375 archive->maximumUncompressedItemSize_ =
377 archive->stream_.open(path, std::ios::binary);
378
379 if (!archive->stream_)
380 {
381 return std::unexpected(makeError(
383 path,
384 "unable to open the legacy DAT bank"));
385 }
386
387 std::error_code sizeError;
388 archive->fileSize_ = std::filesystem::file_size(path, sizeError);
389
390 if (sizeError)
391 {
392 return std::unexpected(makeError(
394 path,
395 sizeError.message()));
396 }
397
398 std::array<std::byte, PhysicalHeaderSize> header{};
399
400 if (!readExact(archive->stream_, header))
401 {
402 return std::unexpected(makeError(
404 path,
405 "DAT header must contain exactly 28 bytes"));
406 }
407
408 for (std::size_t index = 0; index < ArtechSignature.size(); ++index)
409 {
410 if (std::to_integer<char>(header[index]) !=
412 {
413 return std::unexpected(makeError(
415 path,
416 "missing Artech\\0 signature"));
417 }
418 }
419
420 archive->version_ =
421 {
422 readU16Le(header.data() + 8),
423 readU16Le(header.data() + 10),
424 readU16Le(header.data() + 12),
425 readU16Le(header.data() + 14)
426 };
427
428 if (archive->version_.major != SupportedVersionMajor ||
429 archive->version_.minor != SupportedVersionMinor)
430 {
431 return std::unexpected(makeError(
433 path,
434 "only the source-defined DAT version 2.3 is supported"));
435 }
436
437 const auto rawItemCount = readU32Le(header.data() + 20);
438
439 // Les deux champs reserves sont volontairement ignores : L_Data les
440 // lisait mais ne leur imposait aucune valeur. En revanche, son groupe
441 // d'items exigeait au moins une entree et un DataTag reste sur 16 bits.
442 if (rawItemCount == 0)
443 {
444 return std::unexpected(makeError(
446 path,
447 "DAT index must contain at least one item"));
448 }
449
450 if (rawItemCount >
451 static_cast<std::uint32_t>(MaximumItemCount))
452 {
453 return std::unexpected(makeError(
455 path,
456 "DAT index cannot exceed the 16-bit DataTag space"));
457 }
458
459 archive->checksum_ = readU32Le(header.data() + 24);
460
461 const std::uint64_t indexBytes =
462 static_cast<std::uint64_t>(rawItemCount) *
464 const std::uint64_t dataStart = PhysicalHeaderSize + indexBytes;
465
466 if (dataStart > archive->fileSize_)
467 {
468 return std::unexpected(makeError(
470 path,
471 "DAT index extends past end of file"));
472 }
473
474 archive->items_.reserve(rawItemCount);
475
476 std::array<std::byte, PhysicalIndexRecordSize> record{};
477
478 for (std::uint32_t item = 0; item < rawItemCount; ++item)
479 {
480 if (!readExact(archive->stream_, record))
481 {
482 return std::unexpected(makeError(
484 path,
485 itemDetail("unable to read complete DAT index record", item),
486 static_cast<DataTag>(item)));
487 }
488
489 const auto offset = readU32Le(record.data());
490 const auto uncompressedSize = readU32Le(record.data() + 4);
491 const auto rawType = readU32Le(record.data() + 8);
492 const auto compressedSize = readU32Le(record.data() + 12);
493
494 if (rawType > static_cast<std::uint32_t>(LegacyDataType::MeshX))
495 {
496 return std::unexpected(makeError(
498 path,
499 itemDetail("data type is outside LE_DATA_DataType", item),
500 static_cast<DataTag>(item)));
501 }
502
504 {
505 static_cast<DataTag>(item),
506 static_cast<LegacyDataType>(rawType),
507 offset,
508 uncompressedSize,
509 compressedSize
510 };
511
512 if (!metadata.present())
513 {
514 archive->items_.push_back(metadata);
515 continue;
516 }
517
518 const bool invalidSignedField =
519 offset > static_cast<std::uint32_t>(
520 std::numeric_limits<std::int32_t>::max()) ||
521 uncompressedSize > static_cast<std::uint32_t>(
522 std::numeric_limits<std::int32_t>::max()) ||
523 compressedSize > static_cast<std::uint32_t>(
524 std::numeric_limits<std::int32_t>::max());
525
526 const std::uint64_t itemEnd =
527 static_cast<std::uint64_t>(offset) + compressedSize;
528
529 if (invalidSignedField ||
531 uncompressedSize == 0 ||
532 compressedSize == 0 ||
533 uncompressedSize > options.maximumUncompressedItemSize ||
534 offset < dataStart ||
535 itemEnd > archive->fileSize_)
536 {
537 return std::unexpected(makeError(
539 path,
541 "DAT item offset or size is outside validated bounds",
542 item),
543 static_cast<DataTag>(item)));
544 }
545
546 archive->items_.push_back(metadata);
547 }
548
550 {
551 archive->stream_.clear();
552 archive->stream_.seekg(
553 static_cast<std::streamoff>(PhysicalHeaderSize),
554 std::ios::beg);
555
556 uLong checksum = crc32(0L, Z_NULL, 0);
557 std::array<std::byte, 64U * 1024U> block{};
558 std::uint64_t bytesRemaining =
559 archive->fileSize_ - PhysicalHeaderSize;
560
561 while (bytesRemaining > 0)
562 {
563 const auto amount = static_cast<std::size_t>(std::min<
564 std::uint64_t>(bytesRemaining, block.size()));
565 auto destination = std::span<std::byte>(block).first(amount);
566
567 if (!readExact(archive->stream_, destination))
568 {
569 return std::unexpected(makeError(
571 path,
572 "unable to read DAT bytes for CRC-32 validation"));
573 }
574
575 checksum = crc32(
576 checksum,
577 reinterpret_cast<const Bytef*>(block.data()),
578 static_cast<uInt>(amount));
579 bytesRemaining -= amount;
580 }
581
582 if (static_cast<std::uint32_t>(checksum) != archive->checksum_)
583 {
584 return std::unexpected(makeError(
586 path,
587 "stored CRC-32 does not match bytes after the header"));
588 }
589 }
590
591 archive->stream_.clear();
592 archive->cache_.resize(archive->items_.size());
593 archive->open_ = true;
594 return archive;
595 }
596
597
599 {
600 std::scoped_lock lock(mutex_);
601 for (auto& item : cache_) rawDataPool_->remove(item);
602 cache_.clear();
603
604 if (stream_.is_open())
605 {
606 stream_.close();
607 }
608
609 open_ = false;
610 }
611
612
614 {
615 std::scoped_lock lock(mutex_);
616 return open_;
617 }
618
619
621 {
622 return group_;
623 }
624
625
626 const std::filesystem::path& LegacyDataArchive::path() const noexcept
627 {
628 return path_;
629 }
630
631
633 {
634 return version_;
635 }
636
637
639 {
640 return checksum_;
641 }
642
643
645 {
646 return items_.size();
647 }
648
649
650 std::expected<ArchiveItemMetadata, DataError>
652 {
653 if (static_cast<std::size_t>(tag) >= items_.size())
654 {
655 return std::unexpected(makeError(
657 path_,
658 "DataTag is outside this archive index",
659 tag));
660 }
661
662 return items_[tag];
663 }
664
665
666 std::expected<LegacyDataType, DataError>
668 {
669 const auto item = metadata(tag);
670 if (!item) return std::unexpected(item.error());
671 return item->type;
672 }
673
674
675 std::expected<std::uint32_t, DataError>
677 {
678 const auto item = metadata(tag);
679 if (!item) return std::unexpected(item.error());
680 return item->uncompressedSize;
681 }
682
683
684 std::expected<std::uint32_t, DataError>
686 {
687 const auto bytes = load(tag);
688 if (!bytes) return std::unexpected(bytes.error());
689
690 if ((*bytes)->size() > std::numeric_limits<std::uint32_t>::max())
691 {
692 return std::unexpected(makeError(
694 path_,
695 "loaded DAT item size exceeds the legacy 32-bit range",
696 tag));
697 }
698
699 return static_cast<std::uint32_t>((*bytes)->size());
700 }
701
702
703 std::expected<SharedDataBytes, DataError>
705 {
706 std::scoped_lock lock(mutex_);
707
708 if (!open_)
709 {
710 return std::unexpected(makeError(
712 path_,
713 "cannot load an item from a closed archive",
714 tag));
715 }
716
717 if (static_cast<std::size_t>(tag) >= items_.size())
718 {
719 return std::unexpected(makeError(
721 path_,
722 "DataTag is outside this archive index",
723 tag));
724 }
725
726 if (auto cached = rawDataPool_->acquire(cache_[tag]))
727 {
728 return cached;
729 }
730
731 const auto& item = items_[tag];
732
733 if (!item.present())
734 {
735 return std::unexpected(makeError(
737 path_,
738 "DataTag identifies an empty DAT slot",
739 tag));
740 }
741
742 detail::RawDataReservation reservation(rawDataPool_, item.uncompressedSize);
743 auto compressed = std::vector<Bytef>(item.compressedSize);
744 stream_.clear();
745 stream_.seekg(
746 static_cast<std::streamoff>(item.offset),
747 std::ios::beg);
748
749 if (!stream_ ||
750 !readExact(
751 stream_,
752 std::as_writable_bytes(std::span(compressed))))
753 {
754 return std::unexpected(makeError(
756 path_,
757 "unable to read the complete compressed DAT item",
758 tag));
759 }
760
761 auto output = std::make_shared<DataBytes>(item.uncompressedSize);
762 uLongf outputSize = item.uncompressedSize;
763 uLong compressedBytesConsumed = item.compressedSize;
764
765 const int result = uncompress2(
766 reinterpret_cast<Bytef*>(output->data()),
767 &outputSize,
768 compressed.data(),
770
771 if (result != Z_OK)
772 {
773 std::ostringstream detail;
774 detail
775 << "zlib inflate failed with code "
776 << result;
777
778 return std::unexpected(makeError(
780 path_,
781 detail.str(),
782 tag));
783 }
784
785 if (outputSize != item.uncompressedSize ||
786 compressedBytesConsumed != item.compressedSize)
787 {
788 return std::unexpected(makeError(
790 path_,
791 "inflated size or consumed compressed size differs from index",
792 tag));
793 }
794
795 cache_[tag] = output;
796 return reservation.publish(std::move(output));
797 }
798
799
800 std::expected<std::size_t, DataError> LegacyDataArchive::readRaw(
801 DataTag tag,
802 std::span<std::byte> destination,
803 std::uint32_t startOffset)
804 {
805 std::scoped_lock lock(mutex_);
806
807 if (!open_)
808 {
809 return std::unexpected(makeError(
811 path_,
812 "cannot read raw bytes from a closed archive",
813 tag));
814 }
815
816 if (static_cast<std::size_t>(tag) >= items_.size())
817 {
818 return std::unexpected(makeError(
820 path_,
821 "DataTag is outside this archive index",
822 tag));
823 }
824
825 const auto& item = items_[tag];
826 if (!item.present())
827 {
828 return std::unexpected(makeError(
830 path_,
831 "DataTag identifies an empty DAT slot",
832 tag));
833 }
834
835 if (destination.empty() || startOffset >= item.uncompressedSize)
836 return std::size_t{0};
837
838 const auto amount = std::min<std::size_t>(
839 destination.size(),
840 static_cast<std::size_t>(item.uncompressedSize - startOffset));
841
842 std::vector<Bytef> compressed(item.compressedSize);
843 stream_.clear();
844 stream_.seekg(
845 static_cast<std::streamoff>(item.offset),
846 std::ios::beg);
847
848 if (!stream_ ||
849 !readExact(
850 stream_,
851 std::as_writable_bytes(std::span(compressed))))
852 {
853 return std::unexpected(makeError(
855 path_,
856 "unable to read the complete compressed DAT item",
857 tag));
858 }
859
861 inflater.next_in = compressed.data();
862 inflater.avail_in = static_cast<uInt>(compressed.size());
863 if (inflateInit(&inflater) != Z_OK)
864 {
865 return std::unexpected(makeError(
867 path_,
868 "zlib inflate initialization failed",
869 tag));
870 }
871
872 struct InflateGuard
873 {
874 z_stream* stream{};
876 {
877 if (stream) inflateEnd(stream);
878 }
879 } guard{&inflater};
880
881 std::array<std::byte, 64U * 1024U> block{};
882 std::uint64_t producedTotal{};
883 std::size_t copied{};
884 const std::uint64_t wantedStart = startOffset;
885 const std::uint64_t wantedEnd =
886 wantedStart + static_cast<std::uint64_t>(amount);
887
888 while (copied < amount)
889 {
890 inflater.next_out =
891 reinterpret_cast<Bytef*>(block.data());
892 inflater.avail_out = static_cast<uInt>(block.size());
893
894 const int result = inflate(&inflater, Z_NO_FLUSH);
895 if (result != Z_OK && result != Z_STREAM_END)
896 {
897 return std::unexpected(makeError(
899 path_,
900 "zlib inflate failed during raw slice read",
901 tag));
902 }
903
904 const auto produced = block.size() - inflater.avail_out;
905 const std::uint64_t blockStart = producedTotal;
906 const std::uint64_t blockEnd =
907 producedTotal + static_cast<std::uint64_t>(produced);
908
910 {
911 const auto overlapStart =
912 std::max(blockStart, wantedStart);
913 const auto overlapEnd =
914 std::min(blockEnd, wantedEnd);
915 const auto sourceOffset =
916 static_cast<std::size_t>(overlapStart - blockStart);
917 const auto copyCount =
918 static_cast<std::size_t>(overlapEnd - overlapStart);
919 std::memcpy(
920 destination.data() + copied,
921 block.data() + sourceOffset,
922 copyCount);
923 copied += copyCount;
924 }
925
927
928 if (result == Z_STREAM_END)
929 break;
930
931 if (produced == 0 && inflater.avail_in == 0)
932 {
933 return std::unexpected(makeError(
935 path_,
936 "compressed DAT item ended before the requested raw slice",
937 tag));
938 }
939 }
940
941 if (copied != amount)
942 {
943 return std::unexpected(makeError(
945 path_,
946 "decompressed DAT item ended before the requested raw slice",
947 tag));
948 }
949
950 return copied;
951 }
952
953
954 std::expected<bool, DataError> LegacyDataArchive::isLoaded(DataTag tag) const
955 {
956 std::scoped_lock lock(mutex_);
957
958 if (!open_)
959 {
960 return std::unexpected(makeError(
962 path_,
963 "cannot query loaded state from a closed archive",
964 tag));
965 }
966
967 if (static_cast<std::size_t>(tag) >= items_.size())
968 {
969 return std::unexpected(makeError(
971 path_,
972 "DataTag is outside this archive index",
973 tag));
974 }
975
976 if (!items_[tag].present())
977 return false;
978
979 return !cache_[tag].expired();
980 }
981
982
983 std::expected<bool, DataError> LegacyDataArchive::unload(DataTag tag)
984 {
985 std::scoped_lock lock(mutex_);
986
987 if (!open_)
988 {
989 return std::unexpected(makeError(
991 path_,
992 "cannot unload an item from a closed archive",
993 tag));
994 }
995
996 if (static_cast<std::size_t>(tag) >= items_.size())
997 {
998 return std::unexpected(makeError(
1000 path_,
1001 "DataTag is outside this archive index",
1002 tag));
1003 }
1004
1005 const bool wasCached = rawDataPool_->remove(cache_[tag]);
1006 cache_[tag].reset();
1007 return wasCached;
1008 }
1009
1010
1012 {
1013 std::scoped_lock lock(mutex_);
1014
1015 for (auto& item : cache_)
1016 {
1017 rawDataPool_->remove(item);
1018 item.reset();
1019 }
1020 }
1021
1022
1024 {
1025 std::scoped_lock lock(mutex_);
1026 return static_cast<std::size_t>(std::count_if(
1027 cache_.begin(),
1028 cache_.end(),
1029 [](const std::weak_ptr<const DataBytes>& item)
1030 {
1031 return !item.expired();
1032 }));
1033 }
1034
1035
1036 std::expected<std::shared_ptr<LegacyDataArchive>, DataError>
1038 const std::filesystem::path& path,
1039 std::uint16_t group,
1040 ArchiveOpenOptions options)
1041 {
1042 {
1043 std::scoped_lock lock(mutex_);
1044
1045 if (archives_.contains(group))
1046 {
1047 return std::unexpected(DataError
1048 {
1050 path,
1051 std::nullopt,
1052 "a DAT archive is already mounted for this group"
1053 });
1054 }
1055 }
1056
1057 auto opened = LegacyDataArchive::open(path, group, options);
1058
1059 if (!opened)
1060 {
1061 return std::unexpected(opened.error());
1062 }
1063
1064 std::scoped_lock lock(mutex_);
1065
1066 const auto [iterator, inserted] = archives_.emplace(group, *opened);
1067
1068 if (!inserted)
1069 {
1070 (*opened)->close();
1071 return std::unexpected(DataError
1072 {
1074 path,
1075 std::nullopt,
1076 "a DAT archive was mounted concurrently for this group"
1077 });
1078 }
1079
1080 return iterator->second;
1081 }
1082
1083
1084 std::expected<std::shared_ptr<LegacyDataArchive>, DataError>
1086 {
1087 std::scoped_lock lock(mutex_);
1088 const auto found = archives_.find(group);
1089
1090 if (found == archives_.end())
1091 {
1092 return std::unexpected(DataError
1093 {
1095 {},
1096 std::nullopt,
1097 "no DAT archive is mounted for this group"
1098 });
1099 }
1100
1101 return found->second;
1102 }
1103
1104
1105 std::expected<ArchiveItemMetadata, DataError>
1107 {
1108 auto mounted = archive(dataGroup(id));
1109
1110 if (!mounted)
1111 {
1112 return std::unexpected(mounted.error());
1113 }
1114
1115 return (*mounted)->metadata(dataTag(id));
1116 }
1117
1118
1119 std::expected<LegacyDataType, DataError>
1121 {
1122 if (isEmptyDataId(id))
1124
1125 auto mounted = archive(dataGroup(id));
1126 if (!mounted)
1127 return std::unexpected(mounted.error());
1128
1129 return (*mounted)->initialDataType(dataTag(id));
1130 }
1131
1132
1133 std::expected<std::uint32_t, DataError>
1135 {
1136 if (isEmptyDataId(id))
1137 return std::uint32_t{0};
1138
1139 auto mounted = archive(dataGroup(id));
1140 if (!mounted)
1141 return std::unexpected(mounted.error());
1142
1143 return (*mounted)->initialSize(dataTag(id));
1144 }
1145
1146
1147 std::expected<std::uint32_t, DataError>
1149 {
1150 if (isEmptyDataId(id))
1151 return std::uint32_t{0};
1152
1153 auto mounted = archive(dataGroup(id));
1154 if (!mounted)
1155 return std::unexpected(mounted.error());
1156
1157 return (*mounted)->loadedRawSize(dataTag(id));
1158 }
1159
1160
1161 std::expected<SharedDataBytes, DataError>
1163 {
1164 auto mounted = archive(dataGroup(id));
1165
1166 if (!mounted)
1167 {
1168 return std::unexpected(mounted.error());
1169 }
1170
1171 return (*mounted)->load(dataTag(id));
1172 }
1173
1174
1175 std::expected<std::size_t, DataError>
1177 DataId id,
1178 std::span<std::byte> destination,
1179 std::uint32_t startOffset) const
1180 {
1181 if (isEmptyDataId(id))
1182 return std::size_t{0};
1183
1184 auto mounted = archive(dataGroup(id));
1185 if (!mounted)
1186 return std::unexpected(mounted.error());
1187
1188 return (*mounted)->readRaw(
1190 }
1191
1192
1193 std::expected<bool, DataError>
1195 {
1196 if (isEmptyDataId(id))
1197 return false;
1198
1199 auto mounted = archive(dataGroup(id));
1200 if (!mounted)
1201 return std::unexpected(mounted.error());
1202
1203 return (*mounted)->isLoaded(dataTag(id));
1204 }
1205
1206
1207 std::expected<void, DataError>
1209 {
1210 if (isEmptyDataId(id))
1211 return {};
1212
1213 auto mounted = archive(dataGroup(id));
1214 if (!mounted)
1215 return std::unexpected(mounted.error());
1216
1217 const auto removed = (*mounted)->unload(dataTag(id));
1218 if (!removed)
1219 return std::unexpected(removed.error());
1220
1221 // LE_DATA_Unload succeeds when the item is already out of memory.
1222 // The bool returned by LegacyDataArchive::unload only reports whether
1223 // cache ownership was actually removed during this call.
1224 return {};
1225 }
1226
1227
1229 {
1230 std::vector<std::shared_ptr<LegacyDataArchive>> archives;
1231 {
1232 std::scoped_lock lock(mutex_);
1233 archives.reserve(archives_.size());
1234 for (const auto& [group, archive] : archives_)
1235 {
1236 static_cast<void>(group);
1237 archives.push_back(archive);
1238 }
1239 }
1240
1241 for (const auto& archive : archives)
1242 archive->clearCache();
1243 }
1244
1245
1247 {
1248 std::vector<std::shared_ptr<LegacyDataArchive>> archives;
1249 {
1250 std::scoped_lock lock(mutex_);
1251 archives.reserve(archives_.size());
1252 for (const auto& [group, archive] : archives_)
1253 {
1254 static_cast<void>(group);
1255 archives.push_back(archive);
1256 }
1257 }
1258
1259 std::size_t total{};
1260 for (const auto& archive : archives)
1261 total += archive->cachedItemCount();
1262 return total;
1263 }
1264
1265
1266 std::expected<void, DataError>
1268 {
1269 std::shared_ptr<LegacyDataArchive> removed;
1270
1271 {
1272 std::scoped_lock lock(mutex_);
1273 const auto found = archives_.find(group);
1274
1275 if (found == archives_.end())
1276 {
1277 return std::unexpected(DataError
1278 {
1280 {},
1281 std::nullopt,
1282 "no DAT archive is mounted for this group"
1283 });
1284 }
1285
1286 removed = std::move(found->second);
1287 archives_.erase(found);
1288 }
1289
1290 removed->close();
1291 return {};
1292 }
1293
1294
1296 {
1297 std::unordered_map<
1298 std::uint16_t,
1299 std::shared_ptr<LegacyDataArchive>> removed;
1300
1301 {
1302 std::scoped_lock lock(mutex_);
1303 removed.swap(archives_);
1304 }
1305
1306 for (auto& [group, archive] : removed)
1307 {
1308 static_cast<void>(group);
1309 archive->close();
1310 }
1311 }
1312
1313
1315 {
1316 std::scoped_lock lock(mutex_);
1317 return archives_.size();
1318 }
1319
1320
1321 std::expected<DataIndexTable, DataError>
1322 DataIndexTable::parse(std::span<const std::byte> bytes)
1323 {
1324 if (bytes.empty() || bytes.size() % PhysicalEntrySize != 0)
1325 {
1326 return std::unexpected(DataError
1327 {
1329 {},
1330 std::nullopt,
1331 "index table size must be a non-zero multiple of 6 bytes"
1332 });
1333 }
1334
1336 table.entries_.reserve(bytes.size() / PhysicalEntrySize);
1337
1338 for (std::size_t offset = 0;
1339 offset < bytes.size();
1341 {
1343 {
1344 readU32Le(bytes.data() + offset),
1345 readU16Le(bytes.data() + offset + 4)
1346 };
1347
1348 if (!table.entries_.empty())
1349 {
1350 const auto previous = table.entries_.back().indexValue;
1351
1352 if (entry.indexValue < previous)
1353 {
1354 return std::unexpected(DataError
1355 {
1357 {},
1358 std::nullopt,
1359 "index entries must be sorted for legacy bsearch"
1360 });
1361 }
1362
1363 if (entry.indexValue == previous)
1364 {
1365 return std::unexpected(DataError
1366 {
1368 {},
1369 std::nullopt,
1370 "duplicate keys make legacy bsearch nondeterministic"
1371 });
1372 }
1373 }
1374
1375 table.entries_.push_back(entry);
1376 }
1377
1378 return table;
1379 }
1380
1381
1382 std::optional<DataTag> DataIndexTable::find(
1383 std::uint32_t indexValue) const noexcept
1384 {
1385 const auto found = std::lower_bound(
1386 entries_.begin(),
1387 entries_.end(),
1388 indexValue,
1389 [](const DataIndexEntry& entry, std::uint32_t key)
1390 {
1391 return entry.indexValue < key;
1392 });
1393
1394 if (found == entries_.end() || found->indexValue != indexValue)
1395 {
1396 return std::nullopt;
1397 }
1398
1399 return found->dataTag;
1400 }
1401
1402
1403 std::span<const DataIndexEntry> DataIndexTable::entries() const noexcept
1404 {
1405 return entries_;
1406 }
1407
1408
1409 std::expected<DataId, DataError> lookupIndexedDataId(
1410 const DataSource& source,
1412 std::uint32_t indexValue)
1413 {
1414 auto metadata = source.metadata(indexTableId);
1415
1416 if (!metadata)
1417 {
1418 return std::unexpected(metadata.error());
1419 }
1420
1422 {
1423 return std::unexpected(DataError
1424 {
1426 {},
1428 "indexed lookup requires a LE_DATA_DataIndexTable item"
1429 });
1430 }
1431
1432 auto bytes = source.load(indexTableId);
1433
1434 if (!bytes)
1435 {
1436 return std::unexpected(bytes.error());
1437 }
1438
1440
1441 if (!table)
1442 {
1443 return std::unexpected(table.error());
1444 }
1445
1446 const auto tag = table->find(indexValue);
1447
1448 if (!tag)
1449 {
1450 return std::unexpected(DataError
1451 {
1453 {},
1454 std::nullopt,
1455 "requested key does not exist in the index table"
1456 });
1457 }
1458
1459 return packDataId(dataGroup(indexTableId), *tag);
1460 }
1461
1462 std::expected<DataId, DataError> lookupIndexedDataIdLegacy(
1463 const DataSource& source,
1465 std::uint32_t indexValue)
1466 {
1467 const auto result = lookupIndexedDataId(
1468 source, indexTableId, indexValue);
1469 if (result) return *result;
1470
1471 if (result.error().code == DataErrorCode::TypeMismatch ||
1472 result.error().code == DataErrorCode::IndexedItemNotFound)
1473 return EmptyDataId;
1474
1475 return std::unexpected(result.error());
1476 }
1477}
std::vector< std::uint8_t > data
std::size_t offset
std::uint16_t group
const char * header
LegacyBitmapMetadata metadata
std::vector< std::byte > bytes
std::shared_ptr< const data::HmdTextureImage > source
std::uint64_t key
unsigned index
std::string path
std::deque< Bytes > output
std::expected< LegacyDataType, DataError > initialDataType(DataId id) const override
std::expected< std::uint32_t, DataError > loadedRawSize(DataId id) const override
std::expected< void, DataError > unmount(std::uint16_t group)
std::size_t cachedItemCount() const noexcept
std::expected< SharedDataBytes, DataError > load(DataId id) const override
std::expected< void, DataError > unload(DataId id) const
std::expected< std::uint32_t, DataError > initialSize(DataId id) const override
std::expected< std::shared_ptr< LegacyDataArchive >, DataError > mount(const std::filesystem::path &path, std::uint16_t group, ArchiveOpenOptions options={})
std::size_t mountedCount() const noexcept
std::expected< bool, DataError > isLoaded(DataId id) const override
std::expected< std::shared_ptr< LegacyDataArchive >, DataError > archive(std::uint16_t group) const
std::expected< std::size_t, DataError > readRaw(DataId id, std::span< std::byte > destination, std::uint32_t startOffset=0) const override
std::expected< ArchiveItemMetadata, DataError > metadata(DataId id) const override
std::optional< DataTag > find(std::uint32_t indexValue) const noexcept
std::span< const DataIndexEntry > entries() const noexcept
static std::expected< DataIndexTable, DataError > parse(std::span< const std::byte > bytes)
static constexpr std::size_t PhysicalEntrySize
ArchiveVersion version() const noexcept
std::size_t itemCount() const noexcept
std::expected< std::uint32_t, DataError > loadedRawSize(DataTag tag)
static constexpr std::size_t PhysicalHeaderSize
const std::filesystem::path & path() const noexcept
static std::expected< std::shared_ptr< LegacyDataArchive >, DataError > open(const std::filesystem::path &path, std::uint16_t group, ArchiveOpenOptions options={})
std::expected< std::uint32_t, DataError > initialSize(DataTag tag) const
std::expected< LegacyDataType, DataError > initialDataType(DataTag tag) const
std::uint32_t storedChecksum() const noexcept
std::expected< bool, DataError > isLoaded(DataTag tag) const
static constexpr std::size_t PhysicalIndexRecordSize
std::expected< ArchiveItemMetadata, DataError > metadata(DataTag tag) const
std::expected< bool, DataError > unload(DataTag tag)
std::expected< SharedDataBytes, DataError > load(DataTag tag)
static constexpr std::uint16_t SupportedVersionMajor
static constexpr std::uint16_t SupportedVersionMinor
static constexpr std::size_t MaximumItemCount
std::size_t cachedItemCount() const noexcept
std::uint16_t group() const noexcept
std::expected< std::size_t, DataError > readRaw(DataTag tag, std::span< std::byte > destination, std::uint32_t startOffset=0)
SharedDataBytes publish(SharedDataBytes data, std::size_t reserved)
SharedDataBytes acquire(const std::weak_ptr< const DataBytes > &cached)
RawDataCacheStats stats() const noexcept
bool remove(const std::weak_ptr< const DataBytes > &cached) noexcept
void released(std::uint64_t identity) noexcept
void cancelReservation(std::size_t size) noexcept
SharedDataBytes publish(SharedDataBytes data)
RawDataReservation(const RawDataReservation &)=delete
RawDataReservation(std::shared_ptr< RawDataPool > pool, std::size_t size)
RawDataReservation & operator=(const RawDataReservation &)=delete
const std::shared_ptr< RawDataPool > & rawDataPool()
std::uint32_t DataId
Definition DataBanks.hpp:10
constexpr DataId EmptyDataId
Definition DataBanks.hpp:13
std::string_view legacyDataTypeName(LegacyDataType type) noexcept
std::vector< std::byte > DataBytes
constexpr DataTag dataTag(DataId id) noexcept
Definition DataBanks.hpp:64
std::uint16_t DataTag
Definition DataBanks.hpp:11
RawDataCacheStats rawDataCacheStats() noexcept
std::expected< DataId, DataError > lookupIndexedDataIdLegacy(const DataSource &source, DataId indexTableId, std::uint32_t indexValue)
std::shared_ptr< const DataBytes > SharedDataBytes
constexpr bool isEmptyDataId(DataId id) noexcept
Definition DataBanks.hpp:83
std::expected< DataId, DataError > lookupIndexedDataId(const DataSource &source, DataId indexTableId, std::uint32_t indexValue)
std::string_view dataErrorCodeName(DataErrorCode code) noexcept
constexpr DataId packDataId(std::uint16_t group, DataTag tag) noexcept
Definition DataBanks.hpp:39
constexpr std::size_t LegacyRawDataCacheBudget
constexpr std::uint16_t dataGroup(DataId id) noexcept
Definition DataBanks.hpp:57
std::shared_ptr< RawDataPool > pool