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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2026 Fernando Sahmkow
#include "../../common/hex.h"
#include "../../common/jenkins.h"
#include "common/root_build_utils.h"
#include "install_root.h"
#include <algorithm>
#include <cstring>
namespace whiteout::storages::casc {
using storages::common::jenkinsHash;
// ── Big-endian read helpers ─────────────────────────────────────────────────
namespace {
u16 readBE16(const u8* p) {
return u16(p[0]) << 8 | u16(p[1]);
}
u32 readBE32(const u8* p) {
return u32(p[0]) << 24 | u32(p[1]) << 16 | u32(p[2]) << 8 | u32(p[3]);
}
/// Read a null-terminated string starting at data[pos].
/// Advances pos past the terminator. Returns empty on overflow.
std::string_view readCString(const u8* data, size_t size, size_t& pos) {
if (pos >= size)
return {};
const u8* start = data + pos;
const u8* end = static_cast<const u8*>(std::memchr(start, 0, size - pos));
if (!end)
return {};
size_t const len = static_cast<size_t>(end - start);
pos += len + 1; // skip past NUL
return {reinterpret_cast<const char*>(start), len};
}
/// Reverse bits in a byte (for install manifest bit-reversal of tag masks).
/// Uses the multiplication trick from CascLib:
/// bits[j] = (byte)((bits[j] * 0x0202020202 & 0x010884422010) % 1023)
inline u8 reverseBits(u8 b) {
return static_cast<u8>((u64(b) * 0x0202020202ULL & 0x010884422010ULL) % 1023);
}
} // anonymous namespace
// ── Parser ──────────────────────────────────────────────────────────────────
std::unique_ptr<InstallRoot> InstallRoot::parse(std::span<const u8> data,
interfaces::WorkerPool* /*pool*/) {
// Minimum header size: 2 (magic) + 1 (version) + 1 (hashSize) + 2 (numTags) + 4 (numFiles) = 10
if (data.size() < 10)
return nullptr;
// Validate magic: 'IN' (0x49, 0x4E).
if (data[0] != 'I' || data[1] != 'N')
return nullptr;
[[maybe_unused]] u8 const version = data[2];
u8 const hashSize = data[3];
u16 const numTags = readBE16(data.data() + 4);
u32 const numFiles = readBE32(data.data() + 6);
// Sanity: hashSize is typically 16 (MD5). Allow 16 or 32.
if (hashSize != 16 && hashSize != 32)
return nullptr;
// Upper bound sanity.
if (numFiles > 10'000'000)
return nullptr;
size_t pos = 10;
u32 const numMaskBytes = (numFiles + 7) / 8;
auto root = std::make_unique<InstallRoot>();
// ── Read tags ───────────────────────────────────────────────────────
// Each tag: CString name + i16 BE type + numMaskBytes bitarray.
// We read them but only need names/types for metadata; the bitarrays
// associate tags with files but we don't use them for file access.
std::vector<std::vector<u8>> tagBits;
tagBits.reserve(numTags);
for (u16 t = 0; t < numTags; ++t) {
auto name = readCString(data.data(), data.size(), pos);
if (name.data() == nullptr)
return nullptr;
if (pos + 2 > data.size())
return nullptr;
i16 const type = static_cast<i16>(readBE16(data.data() + pos));
pos += 2;
if (pos + numMaskBytes > data.size())
return nullptr;
// Read and bit-reverse the mask bytes (CascLib compat).
std::vector<u8> bits(data.data() + pos, data.data() + pos + numMaskBytes);
for (auto& b : bits)
b = reverseBits(b);
pos += numMaskBytes;
InstallTag tag;
tag.name = std::string(name);
tag.type = type;
root->m_tags.push_back(std::move(tag));
tagBits.push_back(std::move(bits));
}
// ── Read file entries ───────────────────────────────────────────────
root->m_entries.reserve(numFiles);
for (u32 i = 0; i < numFiles; ++i) {
auto name = readCString(data.data(), data.size(), pos);
if (name.data() == nullptr)
return nullptr;
if (pos + hashSize + 4 > data.size())
return nullptr;
RootEntry entry{};
// CKey: first 16 bytes of the hash (even if hashSize > 16).
std::memcpy(entry.cKey.data(), data.data() + pos, std::min<size_t>(hashSize, 16));
pos += hashSize;
// Size: 4 bytes big-endian.
entry.fileSize = readBE32(data.data() + pos);
pos += 4;
entry.path = std::string(name);
entry.fileDataId = kInvalidFileDataId;
entry.localeFlags = 0xFFFFFFFF; // All locales.
entry.contentFlags = 0;
// Compute Jenkins hash for path lookup.
auto hash = jenkinsHash(entry.path);
entry.fileNameHash = u64(hash.pc) | (u64(hash.pb) << 32);
// Collect tag names into a locale mask heuristic:
// If a tag of type 0 (locale) matches, use that locale.
// For now we keep All — tags are informational.
root->m_entries.push_back(std::move(entry));
}
if (root->m_entries.empty())
return nullptr;
root->buildIndices();
return root;
}
// ── Lookup ──────────────────────────────────────────────────────────────────
std::vector<const RootEntry*> InstallRoot::findByPath(const std::string& path) const {
return findByPathOrHash(path, m_entries, m_byPath, m_byNameHash);
}
std::vector<const RootEntry*> InstallRoot::findByFileDataId(u32 /*fileDataId*/,
FileIdHint /*hint*/) const {
return {}; // Install manifest does not use FileDataId.
}
// ── Index building ──────────────────────────────────────────────────────────
void InstallRoot::buildIndices() {
buildPathAndHashIndex(m_byPath, m_byNameHash, m_entries);
}
} // namespace whiteout::storages::casc