载入中...
搜索中...
未找到
EvaArchive.cpp
浏览该文件的文档.
1#include "asset/EvaArchive.h"
2
3#include "data/HashFunction.h"
4
5#include <algorithm>
6#include <array>
7#include <cstdlib>
8#include <limits>
9#include <map>
10#include <set>
11#include <tuple>
12
13#include <zlib.h>
14#include <utf8proc.h>
15
16namespace eve::asset {
17namespace {
18
19constexpr std::uint32_t kLocalSignature = 0x04034b50;
20constexpr std::uint32_t kCentralSignature = 0x02014b50;
21constexpr std::uint32_t kZip64EndSignature = 0x06064b50;
22constexpr std::uint32_t kZip64LocatorSignature = 0x07064b50;
23constexpr std::uint32_t kEndSignature = 0x06054b50;
24constexpr std::uint16_t kUtf8Flag = 0x0800;
25
26void put16(std::vector<std::uint8_t>& out, std::uint16_t value) {
27 out.push_back(static_cast<std::uint8_t>(value));
28 out.push_back(static_cast<std::uint8_t>(value >> 8));
29}
30void put32(std::vector<std::uint8_t>& out, std::uint32_t value) {
31 for (unsigned shift = 0; shift != 32; shift += 8) out.push_back(static_cast<std::uint8_t>(value >> shift));
32}
33void put64(std::vector<std::uint8_t>& out, std::uint64_t value) {
34 for (unsigned shift = 0; shift != 64; shift += 8) out.push_back(static_cast<std::uint8_t>(value >> shift));
35}
36
37bool rangeFits(std::size_t offset, std::size_t size, std::size_t extent) {
38 return offset <= extent && size <= extent - offset;
39}
40
41bool get16(std::span<const std::uint8_t> bytes, std::size_t offset, std::uint16_t& value) {
42 if (!rangeFits(offset, 2, bytes.size())) return false;
43 value = static_cast<std::uint16_t>(bytes[offset] | (std::uint16_t(bytes[offset + 1]) << 8));
44 return true;
45}
46bool get32(std::span<const std::uint8_t> bytes, std::size_t offset, std::uint32_t& value) {
47 if (!rangeFits(offset, 4, bytes.size())) return false;
48 value = 0;
49 for (unsigned index = 0; index != 4; ++index) value |= std::uint32_t(bytes[offset + index]) << (index * 8);
50 return true;
51}
52bool get64(std::span<const std::uint8_t> bytes, std::size_t offset, std::uint64_t& value) {
53 if (!rangeFits(offset, 8, bytes.size())) return false;
54 value = 0;
55 for (unsigned index = 0; index != 8; ++index) value |= std::uint64_t(bytes[offset + index]) << (index * 8);
56 return true;
57}
58
59std::uint32_t crcOf(std::span<const std::uint8_t> bytes) {
60 uLong checksum = crc32(0, Z_NULL, 0);
61 while (!bytes.empty()) {
62 const auto count = static_cast<uInt>(std::min<std::size_t>(bytes.size(), std::numeric_limits<uInt>::max()));
63 checksum = crc32(checksum, bytes.data(), count);
64 bytes = bytes.subspan(count);
65 }
66 return static_cast<std::uint32_t>(checksum);
67}
68
69std::string sha256Text(std::span<const std::uint8_t> bytes) {
70 data::HashFunction::Value digest{};
72 ->hash("sha256", reinterpret_cast<const char*>(bytes.data()), bytes.size(), digest);
73 static constexpr char digits[] = "0123456789abcdef";
74 std::string result = "sha256:";
75 result.reserve(71);
76 for (std::size_t index = 0; index < 32; ++index) {
77 const auto byte = static_cast<unsigned char>(digest.data[index]);
78 result.push_back(digits[byte >> 4]);
79 result.push_back(digits[byte & 0x0f]);
80 }
81 return result;
82}
83
84bool validPath(std::string_view path, const EvaArchiveLimits& limits) {
85 if (path.empty() || path.size() > limits.maximumPathBytes ||
86 path.size() > std::numeric_limits<std::uint16_t>::max() || path.front() == '/' || path.back() == '/')
87 return false;
88 std::size_t segmentStart = 0;
89 for (std::size_t index = 0; index <= path.size(); ++index) {
90 if (index < path.size()) {
91 const unsigned char byte = static_cast<unsigned char>(path[index]);
92 if (byte == '\\' || byte == 0 || byte < 0x20 || byte == 0x7f) return false;
93 if (byte != '/') continue;
94 }
95 const std::string_view segment = path.substr(segmentStart, index - segmentStart);
96 if (segment.empty() || segment == "." || segment == "..") return false;
97 segmentStart = index + 1;
98 }
99 for (std::size_t index = 0; index < path.size();) {
100 const unsigned char lead = static_cast<unsigned char>(path[index]);
101 if (lead < 0x80) { ++index; continue; }
102 std::size_t continuation = 0;
103 std::uint32_t codepoint = 0;
104 if ((lead & 0xe0) == 0xc0) { continuation = 1; codepoint = lead & 0x1f; }
105 else if ((lead & 0xf0) == 0xe0) { continuation = 2; codepoint = lead & 0x0f; }
106 else if ((lead & 0xf8) == 0xf0) { continuation = 3; codepoint = lead & 0x07; }
107 else return false;
108 if (continuation >= path.size() - index) return false;
109 for (std::size_t part = 1; part <= continuation; ++part) {
110 const unsigned char byte = static_cast<unsigned char>(path[index + part]);
111 if ((byte & 0xc0) != 0x80) return false;
112 codepoint = (codepoint << 6) | (byte & 0x3f);
113 }
114 if ((continuation == 1 && codepoint < 0x80) || (continuation == 2 && codepoint < 0x800) ||
115 (continuation == 3 && codepoint < 0x10000) || codepoint > 0x10ffff ||
116 (codepoint >= 0xd800 && codepoint <= 0xdfff)) return false;
117 index += continuation + 1;
118 }
119 utf8proc_uint8_t* normalized = nullptr;
120 const auto normalizedSize = utf8proc_map(
121 reinterpret_cast<const utf8proc_uint8_t*>(path.data()),
122 static_cast<utf8proc_ssize_t>(path.size()), &normalized,
123 static_cast<utf8proc_option_t>(UTF8PROC_STABLE | UTF8PROC_COMPOSE));
124 if (normalizedSize < 0 || !normalized) return false;
125 const bool canonical = static_cast<std::size_t>(normalizedSize) == path.size() &&
126 std::equal(path.begin(), path.end(),
127 reinterpret_cast<const char*>(normalized));
128 std::free(normalized);
129 return canonical;
130}
131
132std::string unicodeCaseFold(std::string_view path) {
133 utf8proc_uint8_t* folded = nullptr;
134 const auto foldedSize = utf8proc_map(
135 reinterpret_cast<const utf8proc_uint8_t*>(path.data()),
136 static_cast<utf8proc_ssize_t>(path.size()), &folded,
137 static_cast<utf8proc_option_t>(UTF8PROC_STABLE | UTF8PROC_COMPOSE | UTF8PROC_CASEFOLD));
138 if (foldedSize < 0 || !folded) return {};
139 std::string result(reinterpret_cast<const char*>(folded),
140 static_cast<std::size_t>(foldedSize));
141 std::free(folded);
142 return result;
143}
144
145struct CentralRecord {
146 std::string path;
147 std::uint32_t crc = 0;
148 std::uint64_t decodedSize = 0;
149 std::uint64_t compressedSize = 0;
150 std::uint64_t localOffset = 0;
151 std::uint16_t method = 0;
152};
153
154const Value* objectField(const Value::Object& object, std::string_view name);
155
156std::vector<std::uint8_t> deflateRaw(std::span<const std::uint8_t> input) {
157 if (input.empty() || input.size() > std::numeric_limits<uInt>::max()) return {};
158 z_stream stream{};
159 if (deflateInit2(&stream, Z_BEST_COMPRESSION, Z_DEFLATED, -MAX_WBITS, 8,
160 Z_DEFAULT_STRATEGY) != Z_OK)
161 return {};
162 const uLong bound = deflateBound(&stream, static_cast<uLong>(input.size()));
163 if (bound > std::numeric_limits<uInt>::max()) {
164 deflateEnd(&stream);
165 return {};
166 }
167 std::vector<std::uint8_t> output(static_cast<std::size_t>(bound));
168 stream.next_in = const_cast<Bytef*>(input.data());
169 stream.avail_in = static_cast<uInt>(input.size());
170 stream.next_out = output.data();
171 stream.avail_out = static_cast<uInt>(output.size());
172 const int status = deflate(&stream, Z_FINISH);
173 const std::size_t size = stream.total_out;
174 deflateEnd(&stream);
175 if (status != Z_STREAM_END || size >= input.size()) return {};
176 output.resize(size);
177 return output;
178}
179
180Result<std::vector<std::uint8_t>> inflateRaw(std::span<const std::uint8_t> input,
181 std::uint64_t decodedSize) {
182 if (input.size() > std::numeric_limits<uInt>::max() ||
183 decodedSize > std::numeric_limits<uInt>::max())
184 return Result<std::vector<std::uint8_t>>::failure(Diagnostic::error(
185 DiagnosticCode::InvalidArgument, "Deflate entry exceeds decoder limits", {}, {}, "asset.eva.archive"));
186 std::vector<std::uint8_t> output(static_cast<std::size_t>(decodedSize));
187 std::uint8_t emptyOutput = 0;
188 z_stream stream{};
189 stream.next_in = const_cast<Bytef*>(input.data());
190 stream.avail_in = static_cast<uInt>(input.size());
191 stream.next_out = output.empty() ? &emptyOutput : output.data();
192 stream.avail_out = output.empty() ? 1u : static_cast<uInt>(output.size());
193 if (inflateInit2(&stream, -MAX_WBITS) != Z_OK)
194 return Result<std::vector<std::uint8_t>>::failure(Diagnostic::error(
195 DiagnosticCode::Failed, "Deflate decoder initialization failed", {}, {}, "asset.eva.archive"));
196 const int status = inflate(&stream, Z_FINISH);
197 const bool exact = status == Z_STREAM_END && stream.total_in == input.size() &&
198 stream.total_out == decodedSize;
199 inflateEnd(&stream);
200 if (!exact)
201 return Result<std::vector<std::uint8_t>>::failure(
202 Diagnostic::error(DiagnosticCode::ParseError, "Deflate payload is malformed", {}, {}, "asset.eva.archive"));
203 return Result<std::vector<std::uint8_t>>::success(std::move(output));
204}
205
206Result<void> validateAssetDefinitions(const EvaManifest& manifest,
207 const std::vector<EvaArchiveEntry>& entries,
208 const EvaArchiveLimits& limits) {
209 for (const auto& asset : manifest.assets) {
210 if (!validPath(asset.definition, limits) || asset.definition == "manifest.json")
212 "asset definition path is not canonical",
213 asset.definition, {}, "asset.eva.archive"));
214 const auto found = std::lower_bound(entries.begin(), entries.end(), asset.definition,
215 [](const EvaArchiveEntry& entry, std::string_view path) {
216 return entry.path < path;
217 });
218 if (found == entries.end() || found->path != asset.definition)
220 "asset definition entry is missing",
221 asset.definition, {}, "asset.eva.archive"));
222 if (sha256Text(found->bytes) != asset.contentHash)
224 "asset definition content hash does not match",
225 asset.definition, {}, "asset.eva.archive"));
226 if (found->bytes.size() > limits.maximumManifestBytes)
228 DiagnosticCode::InvalidArgument, "asset definition exceeds metadata budget",
229 asset.definition, {}, "asset.eva.archive"));
230 auto parsed = Value::fromJson(std::string_view(
231 reinterpret_cast<const char*>(found->bytes.data()), found->bytes.size()));
232 if (!parsed) return Result<void>::failure(parsed.status());
233 const auto* object = parsed.value().getIf<Value::Object>();
234 const Value* schema = object ? objectField(*object, "schema") : nullptr;
235 const Value* version = object ? objectField(*object, "schemaVersion") : nullptr;
236 if (!schema || !schema->isString() || schema->asString() != asset.type ||
237 !version || !version->isInt64() || version->asInt() <= 0 ||
238 static_cast<std::uint64_t>(version->asInt()) != asset.schemaVersion.value())
241 "asset definition schema identity does not match manifest", asset.definition,
242 {}, "asset.eva.archive"));
243 }
244 return Result<void>::success();
245}
246
247Result<void> validateContentAddressedBlobs(const std::vector<EvaArchiveEntry>& entries) {
248 constexpr std::string_view prefix = "blobs/sha256/";
249 for (const auto& entry : entries) {
250 if (!entry.path.starts_with(prefix)) continue;
251 const std::string_view digest = std::string_view(entry.path).substr(prefix.size());
252 if (digest.size() != 64 ||
253 !std::all_of(digest.begin(), digest.end(), [](unsigned char character) {
254 return (character >= '0' && character <= '9') ||
255 (character >= 'a' && character <= 'f');
256 }) || sha256Text(entry.bytes) != "sha256:" + std::string(digest))
259 "content-addressed blob path does not match its bytes", entry.path, {},
260 "asset.eva.archive"));
261 }
262 return Result<void>::success();
263}
264
265const Value* objectField(const Value::Object& object, std::string_view name) {
266 const auto found = object.find(std::string(name));
267 return found == object.end() ? nullptr : &found->second;
268}
269
270Result<void> validateImportReport(const EvaManifest& manifest,
271 const std::vector<EvaArchiveEntry>& entries,
272 const EvaArchiveLimits& limits) {
273 const Value* pathValue = objectField(manifest.provenance, "path");
274 if (!pathValue) return Result<void>::success();
275 if (!pathValue->isString() || pathValue->asString() != "reports/import.json")
277 DiagnosticCode::ParseError, "provenance report path must be reports/import.json",
278 "$.provenance.path", {}, "asset.eva.archive"));
279 const auto found = std::lower_bound(entries.begin(), entries.end(), pathValue->asString(),
280 [](const EvaArchiveEntry& entry, std::string_view path) {
281 return entry.path < path;
282 });
283 if (found == entries.end() || found->path != pathValue->asString())
285 DiagnosticCode::NotFound, "manifest provenance report is missing", pathValue->asString(), {},
286 "asset.eva.archive"));
287 if (found->bytes.size() > limits.maximumManifestBytes)
289 DiagnosticCode::InvalidArgument, "import report exceeds admission budget", found->path, {},
290 "asset.eva.archive"));
291 auto parsed = Value::fromJson(std::string(found->bytes.begin(), found->bytes.end()));
292 if (!parsed) return Result<void>::failure(parsed.status());
293 const auto* report = parsed.value().getIf<Value::Object>();
294 const Value* schema = report ? objectField(*report, "schema") : nullptr;
295 const Value* version = report ? objectField(*report, "schemaVersion") : nullptr;
296 const Value* importKey = report ? objectField(*report, "importKey") : nullptr;
297 const Value* mappings = report ? objectField(*report, "sourceObjects") : nullptr;
298 const auto* mappingArray = mappings ? mappings->getIf<Value::Array>() : nullptr;
299 const Value* packageName = report ? objectField(*report, "packageName") : nullptr;
300 const Value* packageVersion = report ? objectField(*report, "packageVersion") : nullptr;
301 const Value* canonicalAssets = report ? objectField(*report, "canonicalAssets") : nullptr;
302 const auto* canonicalAssetArray = canonicalAssets ? canonicalAssets->getIf<Value::Array>() : nullptr;
303 const Value* findings = report ? objectField(*report, "findings") : nullptr;
304 const auto* findingArray = findings ? findings->getIf<Value::Array>() : nullptr;
305 const Value* counts = report ? objectField(*report, "counts") : nullptr;
306 const auto* countObject = counts ? counts->getIf<Value::Object>() : nullptr;
307 const Value* manifestKey = objectField(manifest.provenance, "importKey");
308 if (!schema || !schema->isString() || schema->asString() != "eve.asset-import-report" ||
309 !version || !version->isInt64() || version->asInt() != 1 || !importKey ||
310 !importKey->isString() || !manifestKey || !manifestKey->isString() ||
311 importKey->asString() != manifestKey->asString() || !mappingArray || !packageName ||
312 !packageName->isString() || packageName->asString() != manifest.packageName ||
313 !packageVersion || !packageVersion->isString() ||
314 packageVersion->asString() != manifest.packageVersion || !canonicalAssetArray ||
315 !findingArray || !countObject)
317 DiagnosticCode::ParseError, "import report envelope or import key is invalid", found->path, {},
318 "asset.eva.archive"));
319 Value::Object keyFacts = *report;
320 keyFacts.erase("importKey");
321 auto canonicalFacts = Value(std::move(keyFacts)).toJson();
322 if (!canonicalFacts) return Result<void>::failure(canonicalFacts.status());
323 const std::string computedKey = sha256Text(std::span<const std::uint8_t>(
324 reinterpret_cast<const std::uint8_t*>(canonicalFacts.value().data()),
325 canonicalFacts.value().size()));
326 if (computedKey != importKey->asString())
328 DiagnosticCode::HashMismatch, "import report key does not match canonical facts", found->path, {},
329 "asset.eva.archive"));
330 std::set<PersistentId> local;
331 for (const auto& asset : manifest.assets) local.emplace(asset.asset.id());
332 std::set<std::pair<std::string, PersistentId>> uniqueMappings;
333 for (std::size_t index = 0; index < mappingArray->size(); ++index) {
334 const auto* mapping = (*mappingArray)[index].getIf<Value::Object>();
335 const Value* source = mapping ? objectField(*mapping, "sourceObject") : nullptr;
336 const Value* asset = mapping ? objectField(*mapping, "asset") : nullptr;
337 if (!source || !source->isString() || source->asString().empty() || !asset ||
338 !asset->isString())
340 DiagnosticCode::ParseError, "import source mapping is malformed", found->path, {},
341 "asset.eva.archive"));
342 auto reference = AssetRef::parse(asset->asString());
343 if (!reference || !local.contains(reference.value().id()) ||
344 !uniqueMappings.emplace(source->asString(), reference.value().id()).second)
346 DiagnosticCode::NotFound, "import source mapping is duplicate or targets a missing asset", asset->asString(), {},
347 "asset.eva.archive"));
348 }
349 using AssetFact = std::tuple<PersistentId, std::string, std::string>;
350 std::set<AssetFact> expectedAssets;
351 for (const auto& asset : manifest.assets)
352 expectedAssets.emplace(asset.asset.id(),
353 asset.type + "/" + std::to_string(asset.schemaVersion.value()),
354 asset.contentHash);
355 std::set<AssetFact> reportedAssets;
356 for (std::size_t index = 0; index < canonicalAssetArray->size(); ++index) {
357 const auto* fact = (*canonicalAssetArray)[index].getIf<Value::Object>();
358 const Value* asset = fact ? objectField(*fact, "asset") : nullptr;
359 const Value* type = fact ? objectField(*fact, "type") : nullptr;
360 const Value* hash = fact ? objectField(*fact, "contentHash") : nullptr;
361 if (!asset || !asset->isString() || !type || !type->isString() || !hash ||
362 !hash->isString())
364 DiagnosticCode::ParseError, "import canonical asset fact is malformed",
365 found->path, {}, "asset.eva.archive"));
366 auto reference = AssetRef::parse(asset->asString());
367 if (!reference ||
368 !reportedAssets.emplace(reference.value().id(), type->asString(), hash->asString()).second)
370 DiagnosticCode::Conflict, "import canonical asset fact is duplicate or invalid",
371 asset->asString(), {}, "asset.eva.archive"));
372 }
373 if (reportedAssets != expectedAssets)
376 "import canonical asset facts do not match the admitted manifest", found->path, {},
377 "asset.eva.archive"));
378 static constexpr std::array<std::string_view, 4> dispositions = {
379 "translated", "baked", "preserved-source", "unsupported"};
380 std::map<std::string, std::int64_t> computedCounts;
381 for (const auto name : dispositions) computedCounts.emplace(name, 0);
382 for (const auto& finding : *findingArray) {
383 const auto* fact = finding.getIf<Value::Object>();
384 const Value* disposition = fact ? objectField(*fact, "disposition") : nullptr;
385 if (!disposition || !disposition->isString() ||
386 !computedCounts.contains(disposition->asString()))
388 DiagnosticCode::ParseError, "import finding disposition is invalid",
389 found->path, {}, "asset.eva.archive"));
390 ++computedCounts[disposition->asString()];
391 }
392 if (countObject->size() != dispositions.size())
394 DiagnosticCode::ParseError, "import finding counts are incomplete",
395 found->path, {}, "asset.eva.archive"));
396 for (const auto name : dispositions) {
397 const Value* count = objectField(*countObject, name);
398 if (!count || !count->isInt64() || count->asInt() != computedCounts.at(std::string(name)))
400 DiagnosticCode::HashMismatch, "import finding count does not match findings",
401 std::string(name), {}, "asset.eva.archive"));
402 }
403 return Result<void>::success();
404}
405
406} // namespace
407
409 std::vector<EvaArchiveEntry> entries,
410 const EvaArchiveLimits& limits) {
411 auto manifestJson = serializeEvaManifest(manifest);
412 if (!manifestJson) return Result<std::vector<std::uint8_t>>::failure(manifestJson.status());
413 std::string manifestText = std::move(manifestJson).takeValue();
414 auto validatedManifest = parseEvaManifest(manifestText);
415 if (!validatedManifest) return Result<std::vector<std::uint8_t>>::failure(validatedManifest.status());
416 if (manifestText.size() > limits.maximumManifestBytes)
418 "manifest exceeds admission budget",
419 "manifest.json", {}, "asset.eva.archive"));
420 entries.push_back({"manifest.json", {manifestText.begin(), manifestText.end()}});
421 if (entries.size() > limits.maximumEntries)
423 DiagnosticCode::InvalidArgument, "archive has too many entries", {}, {}, "asset.eva.archive"));
424 std::sort(entries.begin(), entries.end(), [](const auto& left, const auto& right) {
425 return left.path < right.path;
426 });
427 std::uint64_t total = 0;
428 std::string previous;
429 std::set<std::string> foldedPaths;
430 for (const auto& entry : entries) {
431 if (!validPath(entry.path, limits))
432 return Result<std::vector<std::uint8_t>>::failure(Diagnostic::error(
433 DiagnosticCode::InvalidArgument, "entry path is not canonical", entry.path, {}, "asset.eva.archive"));
434 if (entry.path == previous)
436 DiagnosticCode::Conflict, "duplicate archive entry", entry.path, {}, "asset.eva.archive"));
437 previous = entry.path;
438 const std::string folded = unicodeCaseFold(entry.path);
439 if (folded.empty())
441 "entry path cannot be case-folded",
442 entry.path, {}, "asset.eva.archive"));
443 if (!foldedPaths.emplace(folded).second)
445 DiagnosticCode::Conflict, "case-colliding archive entry", entry.path, {}, "asset.eva.archive"));
446 if (entry.bytes.size() > limits.maximumEntryBytes || entry.bytes.size() > limits.maximumDecodedBytes ||
447 total > limits.maximumDecodedBytes - entry.bytes.size())
449 DiagnosticCode::InvalidArgument, "decoded entry budget exceeded", entry.path, {}, "asset.eva.archive"));
450 total += entry.bytes.size();
451 }
452 auto definitions = validateAssetDefinitions(validatedManifest.value(), entries, limits);
453 if (!definitions) return Result<std::vector<std::uint8_t>>::failure(definitions.status());
454 auto blobs = validateContentAddressedBlobs(entries);
455 if (!blobs) return Result<std::vector<std::uint8_t>>::failure(blobs.status());
456 auto report = validateImportReport(validatedManifest.value(), entries, limits);
457 if (!report) return Result<std::vector<std::uint8_t>>::failure(report.status());
458
459 std::vector<std::uint8_t> out;
460 std::vector<CentralRecord> records;
461 for (const auto& entry : entries) {
462 auto compressed = deflateRaw(entry.bytes);
463 const bool useDeflate = !compressed.empty();
464 const auto payload = useDeflate ? std::span<const std::uint8_t>(compressed)
465 : std::span<const std::uint8_t>(entry.bytes);
466 CentralRecord record{entry.path, crcOf(entry.bytes), entry.bytes.size(), payload.size(),
467 out.size(), static_cast<std::uint16_t>(useDeflate ? 8 : 0)};
468 put32(out, kLocalSignature); put16(out, 45); put16(out, kUtf8Flag); put16(out, record.method);
469 put16(out, 0); put16(out, 0x21); put32(out, record.crc);
470 put32(out, 0xffffffff); put32(out, 0xffffffff);
471 put16(out, static_cast<std::uint16_t>(entry.path.size())); put16(out, 20);
472 out.insert(out.end(), entry.path.begin(), entry.path.end());
473 put16(out, 0x0001); put16(out, 16); put64(out, record.decodedSize);
474 put64(out, record.compressedSize);
475 out.insert(out.end(), payload.begin(), payload.end());
476 records.push_back(std::move(record));
477 }
478 const std::uint64_t centralOffset = out.size();
479 for (const auto& record : records) {
480 put32(out, kCentralSignature); put16(out, 45); put16(out, 45); put16(out, kUtf8Flag);
481 put16(out, record.method);
482 put16(out, 0); put16(out, 0x21); put32(out, record.crc);
483 put32(out, 0xffffffff); put32(out, 0xffffffff);
484 put16(out, static_cast<std::uint16_t>(record.path.size())); put16(out, 28); put16(out, 0);
485 put16(out, 0); put16(out, 0); put32(out, 0); put32(out, 0xffffffff);
486 out.insert(out.end(), record.path.begin(), record.path.end());
487 put16(out, 0x0001); put16(out, 24); put64(out, record.decodedSize);
488 put64(out, record.compressedSize);
489 put64(out, record.localOffset);
490 }
491 const std::uint64_t centralSize = out.size() - centralOffset;
492 const std::uint64_t zip64EndOffset = out.size();
493 put32(out, kZip64EndSignature); put64(out, 44); put16(out, 45); put16(out, 45);
494 put32(out, 0); put32(out, 0); put64(out, records.size()); put64(out, records.size());
495 put64(out, centralSize); put64(out, centralOffset);
496 put32(out, kZip64LocatorSignature); put32(out, 0); put64(out, zip64EndOffset); put32(out, 1);
497 put32(out, kEndSignature); put16(out, 0); put16(out, 0); put16(out, 0xffff); put16(out, 0xffff);
498 put32(out, 0xffffffff); put32(out, 0xffffffff); put16(out, 0);
499 if (out.size() > limits.maximumArchiveBytes)
501 DiagnosticCode::InvalidArgument, "encoded archive exceeds admission budget", {}, {}, "asset.eva.archive"));
502 return Result<std::vector<std::uint8_t>>::success(std::move(out));
503}
504
505Result<EvaArchive> parseEvaArchive(std::span<const std::uint8_t> bytes, const EvaArchiveLimits& limits) {
506 if (bytes.size() > limits.maximumArchiveBytes || bytes.size() < 98)
508 DiagnosticCode::ParseError, "archive size is outside admission limits", {}, {}, "asset.eva.archive"));
509 std::uint32_t signature = 0;
510 std::uint16_t comment = 0;
511 std::size_t eocd = std::numeric_limits<std::size_t>::max();
512 const std::size_t earliest = bytes.size() > 22 + std::numeric_limits<std::uint16_t>::max()
513 ? bytes.size() - 22 - std::numeric_limits<std::uint16_t>::max()
514 : 0;
515 for (std::size_t candidate = bytes.size() - 22;; --candidate) {
516 if (get32(bytes, candidate, signature) && signature == kEndSignature &&
517 get16(bytes, candidate + 20, comment) &&
518 std::size_t(comment) == bytes.size() - candidate - 22) {
519 eocd = candidate;
520 break;
521 }
522 if (candidate == earliest) break;
523 }
524 if (eocd == std::numeric_limits<std::size_t>::max() || eocd < 20)
526 DiagnosticCode::ParseError, "canonical ZIP end record is missing", {}, {}, "asset.eva.archive"));
527 const std::size_t locator = eocd - 20;
528 std::uint64_t zip64Offset = 0;
529 if (!get32(bytes, locator, signature) || signature != kZip64LocatorSignature ||
530 !get64(bytes, locator + 8, zip64Offset) || zip64Offset > locator)
532 Diagnostic::error(DiagnosticCode::ParseError, "ZIP64 locator is invalid", {}, {}, "asset.eva.archive"));
533 std::uint64_t entryCount = 0, centralSize = 0, centralOffset = 0;
534 if (!get32(bytes, static_cast<std::size_t>(zip64Offset), signature) || signature != kZip64EndSignature ||
535 !get64(bytes, static_cast<std::size_t>(zip64Offset) + 32, entryCount) ||
536 !get64(bytes, static_cast<std::size_t>(zip64Offset) + 40, centralSize) ||
537 !get64(bytes, static_cast<std::size_t>(zip64Offset) + 48, centralOffset) ||
538 entryCount > limits.maximumEntries || centralOffset > bytes.size() ||
539 centralSize > bytes.size() - centralOffset || centralOffset + centralSize != zip64Offset)
541 DiagnosticCode::ParseError, "ZIP64 central directory is invalid", {}, {}, "asset.eva.archive"));
542
543 std::vector<CentralRecord> records;
544 std::set<std::string> names;
545 std::set<std::string> foldedNames;
546 std::uint64_t decodedTotal = 0;
547 std::size_t cursor = static_cast<std::size_t>(centralOffset);
548 for (std::uint64_t index = 0; index < entryCount; ++index) {
549 std::uint16_t flags = 0, method = 0, nameSize = 0, extraSize = 0, commentSize = 0, disk = 0;
550 std::uint32_t crc = 0, external = 0;
551 if (!get32(bytes, cursor, signature) || signature != kCentralSignature ||
552 !get16(bytes, cursor + 8, flags) || !get16(bytes, cursor + 10, method) ||
553 !get32(bytes, cursor + 16, crc) || !get16(bytes, cursor + 28, nameSize) ||
554 !get16(bytes, cursor + 30, extraSize) || !get16(bytes, cursor + 32, commentSize) ||
555 !get16(bytes, cursor + 34, disk) || !get32(bytes, cursor + 38, external) ||
556 flags != kUtf8Flag || (method != 0 && method != 8) || commentSize != 0 || disk != 0 || external != 0 ||
557 !rangeFits(cursor + 46, std::size_t(nameSize) + extraSize, bytes.size()))
559 DiagnosticCode::Unsupported, "unsupported or malformed ZIP entry", {}, {}, "asset.eva.archive"));
560 std::string path(reinterpret_cast<const char*>(bytes.data() + cursor + 46), nameSize);
561 const std::string folded = unicodeCaseFold(path);
562 if (!validPath(path, limits) || !names.emplace(path).second || folded.empty() ||
563 !foldedNames.emplace(folded).second)
565 "duplicate or non-canonical entry path", path, {},
566 "asset.eva.archive"));
567 const std::size_t extra = cursor + 46 + nameSize;
568 std::uint16_t extraId = 0, zip64Size = 0;
569 std::uint64_t decodedSize = 0, compressedSize = 0, localOffset = 0;
570 if (extraSize != 28 || !get16(bytes, extra, extraId) || extraId != 1 ||
571 !get16(bytes, extra + 2, zip64Size) || zip64Size != 24 ||
572 !get64(bytes, extra + 4, decodedSize) || !get64(bytes, extra + 12, compressedSize) ||
573 !get64(bytes, extra + 20, localOffset) ||
574 localOffset >= centralOffset ||
575 decodedSize > limits.maximumEntryBytes || decodedSize > limits.maximumDecodedBytes ||
576 decodedTotal > limits.maximumDecodedBytes - decodedSize)
578 DiagnosticCode::ParseError, "ZIP64 entry sizes are invalid", path, {}, "asset.eva.archive"));
579 decodedTotal += decodedSize;
580 records.push_back({std::move(path), crc, decodedSize, compressedSize, localOffset, method});
581 cursor += 46 + nameSize + extraSize;
582 }
583 if (cursor != zip64Offset)
585 DiagnosticCode::ParseError, "central directory length mismatch", {}, {}, "asset.eva.archive"));
586
587 EvaArchive archive;
588 for (const auto& record : records) {
589 const auto local = static_cast<std::size_t>(record.localOffset);
590 std::uint16_t flags = 0, method = 0, nameSize = 0, extraSize = 0;
591 if (!get32(bytes, local, signature) || signature != kLocalSignature ||
592 !get16(bytes, local + 6, flags) || !get16(bytes, local + 8, method) ||
593 !get16(bytes, local + 26, nameSize) || !get16(bytes, local + 28, extraSize) ||
594 flags != kUtf8Flag || method != record.method || (method != 0 && method != 8) || extraSize != 20 ||
595 !rangeFits(local + 30, std::size_t(nameSize) + extraSize, bytes.size()))
597 DiagnosticCode::ParseError, "local ZIP header is invalid", record.path, {}, "asset.eva.archive"));
598 const std::string localName(reinterpret_cast<const char*>(bytes.data() + local + 30), nameSize);
599 std::uint16_t extraId = 0, zip64Size = 0;
600 std::uint64_t decodedSize = 0, compressedSize = 0;
601 const std::size_t extra = local + 30 + nameSize;
602 if (localName != record.path || !get16(bytes, extra, extraId) || extraId != 1 ||
603 !get16(bytes, extra + 2, zip64Size) || zip64Size != 16 ||
604 !get64(bytes, extra + 4, decodedSize) || !get64(bytes, extra + 12, compressedSize) ||
605 decodedSize != record.decodedSize || compressedSize != record.compressedSize)
607 "local and central ZIP records disagree", record.path,
608 {}, "asset.eva.archive"));
609 const std::size_t payload = extra + extraSize;
610 if (record.compressedSize > bytes.size() ||
611 !rangeFits(payload, static_cast<std::size_t>(record.compressedSize), bytes.size()) ||
612 payload + static_cast<std::size_t>(record.compressedSize) > centralOffset)
614 DiagnosticCode::ParseError, "entry payload is truncated", record.path, {}, "asset.eva.archive"));
615 auto payloadBytes = bytes.subspan(payload, static_cast<std::size_t>(record.compressedSize));
616 std::vector<std::uint8_t> decoded;
617 if (record.method == 0) {
618 if (record.compressedSize != record.decodedSize)
620 DiagnosticCode::ParseError, "stored entry sizes disagree", record.path, {}, "asset.eva.archive"));
621 decoded.assign(payloadBytes.begin(), payloadBytes.end());
622 } else {
623 auto inflated = inflateRaw(payloadBytes, record.decodedSize);
624 if (!inflated) return Result<EvaArchive>::failure(inflated.status());
625 decoded = std::move(inflated).takeValue();
626 }
627 if (crcOf(decoded) != record.crc)
629 DiagnosticCode::HashMismatch, "entry CRC does not match", record.path, {}, "asset.eva.archive"));
630 EvaArchiveEntry entry{record.path, std::move(decoded)};
631 if (record.path == "manifest.json") {
632 if (record.decodedSize > limits.maximumManifestBytes)
634 DiagnosticCode::InvalidArgument, "manifest exceeds budget", record.path, {}, "asset.eva.archive"));
635 std::string json(entry.bytes.begin(), entry.bytes.end());
636 auto manifest = parseEvaManifest(json);
637 if (!manifest) return Result<EvaArchive>::failure(manifest.status());
638 archive.manifest = std::move(manifest).takeValue();
639 } else {
640 archive.entries.push_back(std::move(entry));
641 }
642 }
643 if (!names.contains("manifest.json"))
645 DiagnosticCode::ParseError, "root manifest.json is required", {}, {}, "asset.eva.archive"));
646 std::sort(archive.entries.begin(), archive.entries.end(), [](const auto& left, const auto& right) {
647 return left.path < right.path;
648 });
649 auto definitions = validateAssetDefinitions(archive.manifest, archive.entries, limits);
651 auto blobs = validateContentAddressedBlobs(archive.entries);
652 if (!blobs) return Result<EvaArchive>::failure(blobs.status());
653 auto report = validateImportReport(archive.manifest, archive.entries, limits);
654 if (!report) return Result<EvaArchive>::failure(report.status());
655 return Result<EvaArchive>::success(std::move(archive));
656}
657
658} // namespace eve::asset
double value
Value::Object payload
std::string packageName
std::string packageVersion
std::string output
AuthorityStoreHandleRef reference
Definition Authority.cpp:24
std::unordered_map< std::string, QuestRuntime > entries
std::uint16_t method
std::uint64_t compressedSize
std::uint64_t localOffset
std::uint64_t decodedSize
std::uint32_t crc
Deterministic and bounded ZIP64 container for EVEngine source assets.
std::array< std::uint8_t, 32 > hash
Definition Evpack.cpp:172
EvpackChunkInput input
Definition Evpack.cpp:170
wgpu::PopErrorScopeStatus status
HexVec3 left
HexVec3 right
std::string local
size_t offset
std::uint64_t bytes
std::string name
graphics::Canvas * previous
std::string path
Definition PlayHost.cpp:110
std::string id
Definition PlayHost.cpp:108
bool found
std::string digest
std::uint32_t count
std::size_t cursor
float size
Definition TreeMesh.cpp:156
std::vector< ImportFinding > findings
uint32_t index
const UnitySourceAsset & source
const AssetImportLimits & limits
const std::map< std::string, Value > & definitions
static Result< AssetRef > parse(std::string_view text)
Parse an asset://<canonical UUID> reference.
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
static Result< Value > fromJson(std::string_view json)
Parse one strict JSON value into an owning Value.
Definition Value.cpp:57
std::map< std::string, Value > Object
Definition Value.h:34
std::vector< Value > Array
Definition Value.h:33
static HashFunction * getHashFunction(std::string function)
Get a HashFunction instance for the given function.
virtual void hash(std::string function, const char *input, uint64_t length, Value &output) const =0
Hash the input, producing an set of bytes as output.
bool validPath(std::string_view path, const AssetImportLimits &limits)
Valid path.
Result< std::vector< std::uint8_t > > buildEvaArchive(const EvaManifest &manifest, std::vector< EvaArchiveEntry > entries, const EvaArchiveLimits &limits)
Build a deterministic ZIP64 .eva, selecting raw Deflate only when smaller than Store.
Result< EvaArchive > parseEvaArchive(std::span< const std::uint8_t > bytes, const EvaArchiveLimits &limits)
Parse and fully verify an untrusted .eva ZIP64 image.
Result< EvaManifest > parseEvaManifest(std::string_view json)
Parse and validate canonical .eva manifest JSON.
Result< std::string > serializeEvaManifest(const EvaManifest &manifest)
Serialize a validated manifest as deterministic compact JSON.
void put16(std::vector< char > &b, uint16_t v)
以小端序写入 uint16 / uint32。
Definition zip_writer.h:51
void put32(std::vector< char > &b, uint32_t v)
Put 32.
Definition zip_writer.h:56
std::variant< std::monostate, std::int64_t, double, std::string, bool > Value
Definition Database.h:26
@ TypeMismatch
A stable reference resolved to a different canonical domain type.
One owning regular-file entry inside an .eva archive.
Definition EvaArchive.h:31
Admission budgets applied before allocating decoded archive entries.
Definition EvaArchive.h:21
Fully admitted source archive with its typed manifest and payloads.
Definition EvaArchive.h:37
std::vector< EvaArchiveEntry > entries
Definition EvaArchive.h:39
Validated, owning .eva manifest.
Definition EvaManifest.h:62
glm::uvec4 counts