1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2026 Fernando Sahmkow
/// @file tvfs_root.h
/// @brief TVFS root manifest parser (prefix-tree, WC3 Reforged).
///
/// Internal header — not part of the public include path.
#pragma once
#include "../tables/flat_hash_map.h"
#include "common/path_trie.h"
#include "root.h"
#include <array>
#include <functional>
#include <mutex>
#include <span>
#include <string>
#include <vector>
namespace whiteout::interfaces {
class WorkerPool;
}
namespace whiteout::storages::casc {
/// Resolver function for VFS sub-manifest data.
/// Given an EKey (eKeySize bytes), returns a view of the decoded TVFS blob, or
/// empty on failure. The resolver owns the storage and must keep it alive for
/// the whole traversal — WoW retail resolves ~280 MB of sub-manifests, half of
/// it a single blob, so handing back copies dominated the parse.
using VfsResolver = std::function<std::span<const u8>(std::span<const u8> eKey)>;
/// Extra work the traversal does while a leaf name is still in cache.
///
/// WoW retail encodes locale/content flags, a FileDataId and a CKey in the leaf
/// name itself. Decoding there rather than in a second pass over the finished
/// table saves re-reading several million scattered strings, and a caller that
/// has a listfile is going to replace every one of those names anyway.
enum class TvfsLeafDecode : u8 {
None, ///< Store leaf names verbatim.
Wow, ///< Decode WoW leaf names into the entry and keep the name.
WowDropPath, ///< Decode WoW leaf names and leave the entry's path empty.
};
class TvfsRoot final : public RootManifest {
public:
/// Parse a single TVFS blob (no sub-container resolution).
/// @param data Raw (BLTE-decoded) TVFS root bytes.
/// @param pool Optional worker pool for parallel index building.
/// @param buildIdx Build the path index. Pass false when a decorator
/// (WowTvfsRoot) builds its own indices — avoids a wasted
/// O(n) pass. Call ensureIndexed() later if needed.
/// @return Parsed root, or nullptr on failure.
static std::unique_ptr<TvfsRoot> parse(std::span<const u8> data,
interfaces::WorkerPool* pool = nullptr,
bool buildIdx = true,
TvfsLeafDecode leafDecode = TvfsLeafDecode::None);
/// Parse a TVFS blob with sub-container resolution (WC3 Reforged multi-VFS).
/// When a leaf entry's EKey matches a known VFS sub-manifest, the entry is
/// treated as a sub-container: a ':' separator is appended to the path and
/// the sub-manifest is recursively parsed with that prefix. This matches
/// CascLib's path reporting behavior.
/// @param data Raw (BLTE-decoded) TVFS root bytes.
/// @param resolver Resolves VFS sub-manifest EKeys to decoded data.
/// @param vfsEKeys EKeys of known VFS sub-manifests (matched by first eKeySize bytes).
/// @param pool Optional worker pool for parallel index building.
/// @param buildIdx Build the path index (see single-arg overload).
static std::unique_ptr<TvfsRoot> parse(std::span<const u8> data, const VfsResolver& resolver,
const std::vector<std::array<u8, 16>>& vfsEKeys,
interfaces::WorkerPool* pool = nullptr,
bool buildIdx = true,
TvfsLeafDecode leafDecode = TvfsLeafDecode::None);
/// Leaf handling this root was parsed with. Decorators use it to tell
/// whether the entries already carry decoded metadata.
TvfsLeafDecode leafDecode() const {
return m_leafDecode;
}
/// Build the path index if it hasn't been built yet. No-op otherwise.
void ensureIndexed(interfaces::WorkerPool* pool = nullptr);
/// Move the entry table out, leaving this root empty. Used by decorators
/// (WowTvfsRoot) that transform the entries in place — avoids a second
/// allocation + copy of a multi-million-element vector.
std::vector<RootEntry> takeEntries();
/// Merge entries from another TvfsRoot into this one.
void merge(const TvfsRoot& other);
// --- RootManifest interface ---
std::vector<const RootEntry*> findByPath(const std::string& path) const override;
std::vector<const RootEntry*> findByNormalizedPath(
const std::string& normalizedPath) const override;
bool hasPath(const std::string& normalizedPath) const override;
std::vector<const RootEntry*> findByFileDataId(
u32 fileDataId, FileIdHint hint = FileIdHint::None) const override;
bool hasFileDataId(u32, FileIdHint = FileIdHint::None) const override {
return false;
}
RootFormat format() const override {
return RootFormat::Tvfs;
}
/// Enumerate all entries whose path starts with @p normalizedPrefix (directory-scoped).
/// The prefix should be a normalized path (lowercase, backslash-separated).
/// Use an empty prefix to enumerate everything. Callback returns false to stop.
void enumerateUnder(const std::string& normalizedPrefix,
std::function<bool(const RootEntry&)> callback) const override;
protected:
const std::vector<RootEntry>& entries() const override {
return m_entries;
}
std::vector<RootEntry>& mutableEntries() override {
return m_entries;
}
private:
std::vector<RootEntry> m_entries;
TvfsLeafDecode m_leafDecode = TvfsLeafDecode::None;
/// Hash-map index for O(1) exact path lookup.
/// Keys are FNV-1a hashes of normalized paths; values are head indices
/// into m_entries. Entries sharing the same path hash are chained via
/// m_chainNext (singly-linked list, UINT32_MAX = end).
FlatHashMap<u32> m_byPathMap;
std::vector<u32> m_chainNext; ///< Parallel to m_entries; links same-hash entries.
/// Flat-array trie for prefix enumeration (lazy-built on first use).
/// enumerateUnder() is const and callable from several threads at once, so
/// the build has to be gated rather than guessed at from m_trie's state.
mutable std::vector<PathTrieNode> m_trie;
mutable std::once_flag m_trieOnce;
void buildIndices(interfaces::WorkerPool* pool = nullptr, bool preNormalized = false);
void ensureTrie() const;
};
} // namespace whiteout::storages::casc