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;
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));
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));
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));
37bool rangeFits(std::size_t
offset, std::size_t
size, std::size_t extent) {
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;
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;
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;
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);
66 return static_cast<std::uint32_t
>(checksum);
69std::string sha256Text(std::span<const std::uint8_t>
bytes) {
70 data::HashFunction::Value
digest{};
73 static constexpr char digits[] =
"0123456789abcdef";
74 std::string result =
"sha256:";
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]);
86 path.size() > std::numeric_limits<std::uint16_t>::max() ||
path.front() ==
'/' ||
path.back() ==
'/')
88 std::size_t segmentStart = 0;
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;
95 const std::string_view segment =
path.substr(segmentStart,
index - segmentStart);
96 if (segment.empty() || segment ==
"." || segment ==
"..")
return false;
97 segmentStart =
index + 1;
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; }
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);
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;
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);
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));
145struct CentralRecord {
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 {};
159 if (deflateInit2(&stream, Z_BEST_COMPRESSION, Z_DEFLATED, -MAX_WBITS, 8,
160 Z_DEFAULT_STRATEGY) != Z_OK)
162 const uLong bound = deflateBound(&stream,
static_cast<uLong
>(
input.size()));
163 if (bound > std::numeric_limits<uInt>::max()) {
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;
180Result<std::vector<std::uint8_t>> inflateRaw(std::span<const std::uint8_t>
input,
182 if (
input.size() > std::numeric_limits<uInt>::max() ||
187 std::uint8_t emptyOutput = 0;
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)
196 const int status = inflate(&stream, Z_FINISH);
197 const bool exact =
status == Z_STREAM_END && stream.total_in ==
input.size() &&
201 return Result<std::vector<std::uint8_t>>::failure(
203 return Result<std::vector<std::uint8_t>>::success(std::move(
output));
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"));
215 [](
const EvaArchiveEntry& entry, std::string_view
path) {
216 return entry.path < path;
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)
229 asset.definition, {},
"asset.eva.archive"));
231 reinterpret_cast<const char*
>(
found->bytes.data()),
found->bytes.size()));
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"));
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"));
266 const auto found =
object.find(std::string(
name));
267 return found ==
object.end() ? nullptr : &
found->second;
270Result<void> validateImportReport(
const EvaManifest& manifest,
271 const std::vector<EvaArchiveEntry>&
entries,
272 const EvaArchiveLimits&
limits) {
273 const Value* pathValue = objectField(manifest.provenance,
"path");
275 if (!pathValue->isString() || pathValue->asString() !=
"reports/import.json")
278 "$.provenance.path", {},
"asset.eva.archive"));
280 [](
const EvaArchiveEntry& entry, std::string_view
path) {
281 return entry.path < path;
286 "asset.eva.archive"));
287 if (
found->bytes.size() >
limits.maximumManifestBytes)
290 "asset.eva.archive"));
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;
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;
305 const Value*
counts = report ? objectField(*report,
"counts") : 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 ||
314 packageVersion->asString() != manifest.packageVersion || !canonicalAssetArray ||
315 !findingArray || !countObject)
318 "asset.eva.archive"));
320 keyFacts.erase(
"importKey");
321 auto canonicalFacts =
Value(std::move(keyFacts)).toJson();
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())
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;
335 const Value*
source = mapping ? objectField(*mapping,
"sourceObject") : nullptr;
336 const Value* asset = mapping ? objectField(*mapping,
"asset") : nullptr;
341 "asset.eva.archive"));
344 !uniqueMappings.emplace(
source->asString(),
reference.value().id()).second)
347 "asset.eva.archive"));
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()),
355 std::set<AssetFact> reportedAssets;
356 for (std::size_t
index = 0;
index < canonicalAssetArray->size(); ++
index) {
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 ||
365 found->path, {},
"asset.eva.archive"));
368 !reportedAssets.emplace(
reference.value().id(),
type->asString(),
hash->asString()).second)
371 asset->asString(), {},
"asset.eva.archive"));
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) {
384 const Value* disposition = fact ? objectField(*fact,
"disposition") : nullptr;
385 if (!disposition || !disposition->isString() ||
386 !computedCounts.contains(disposition->asString()))
389 found->path, {},
"asset.eva.archive"));
390 ++computedCounts[disposition->asString()];
392 if (countObject->size() != dispositions.size())
395 found->path, {},
"asset.eva.archive"));
396 for (
const auto name : dispositions) {
401 std::string(
name), {},
"asset.eva.archive"));
409 std::vector<EvaArchiveEntry>
entries,
413 std::string manifestText = std::move(manifestJson).takeValue();
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()}});
425 return left.path < right.path;
427 std::uint64_t total = 0;
429 std::set<std::string> foldedPaths;
430 for (
const auto& entry :
entries) {
431 if (!validPath(entry.path,
limits))
438 const std::string folded = unicodeCaseFold(entry.path);
441 "entry path cannot be case-folded",
442 entry.path, {},
"asset.eva.archive"));
443 if (!foldedPaths.emplace(folded).second)
446 if (entry.bytes.size() >
limits.maximumEntryBytes || entry.bytes.size() >
limits.maximumDecodedBytes ||
447 total >
limits.maximumDecodedBytes - entry.bytes.size())
450 total += entry.bytes.size();
454 auto blobs = validateContentAddressedBlobs(
entries);
456 auto report = validateImportReport(validatedManifest.value(),
entries,
limits);
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);
476 records.push_back(std::move(record));
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);
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)
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()
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) {
522 if (candidate == earliest)
break;
524 if (eocd == std::numeric_limits<std::size_t>::max() || eocd < 20)
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)
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)
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);
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 ||
556 flags != kUtf8Flag || (
method != 0 &&
method != 8) || commentSize != 0 || disk != 0 || external != 0 ||
557 !rangeFits(
cursor + 46, std::size_t(nameSize) + extraSize,
bytes.size()))
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;
570 if (extraSize != 28 || !get16(
bytes, extra, extraId) || extraId != 1 ||
571 !get16(
bytes, extra + 2, zip64Size) || zip64Size != 24 ||
581 cursor += 46 + nameSize + extraSize;
583 if (
cursor != zip64Offset)
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 ||
594 flags != kUtf8Flag ||
method != record.method || (
method != 0 &&
method != 8) || extraSize != 20 ||
595 !rangeFits(
local + 30, std::size_t(nameSize) + extraSize,
bytes.size()))
598 const std::string localName(
reinterpret_cast<const char*
>(
bytes.data() +
local + 30), nameSize);
599 std::uint16_t extraId = 0, zip64Size = 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 ||
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)
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)
621 decoded.assign(payloadBytes.begin(), payloadBytes.end());
623 auto inflated = inflateRaw(payloadBytes, record.decodedSize);
625 decoded = std::move(inflated).takeValue();
627 if (crcOf(decoded) != record.crc)
631 if (record.path ==
"manifest.json") {
632 if (record.decodedSize >
limits.maximumManifestBytes)
635 std::string json(entry.bytes.begin(), entry.bytes.end());
638 archive.
manifest = std::move(manifest).takeValue();
640 archive.
entries.push_back(std::move(entry));
643 if (!names.contains(
"manifest.json"))
647 return left.path < right.path;
651 auto blobs = validateContentAddressedBlobs(archive.
entries);
std::string packageVersion
AuthorityStoreHandleRef reference
std::unordered_map< std::string, QuestRuntime > entries
std::uint64_t compressedSize
std::uint64_t localOffset
std::uint64_t decodedSize
Deterministic and bounded ZIP64 container for EVEngine source assets.
std::array< std::uint8_t, 32 > hash
wgpu::PopErrorScopeStatus status
graphics::Canvas * previous
std::vector< ImportFinding > findings
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.
Move-only operation result carrying either a value or Status.
static Result success(T value)
Construct a successful result owning value.
static Result failure(Status status)
Construct a failed result from a structured status.
static Result< Value > fromJson(std::string_view json)
Parse one strict JSON value into an owning Value.
std::map< std::string, Value > Object
std::vector< Value > Array
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。
void put32(std::vector< char > &b, uint32_t v)
Put 32.
std::variant< std::monostate, std::int64_t, double, std::string, bool > Value
@ TypeMismatch
A stable reference resolved to a different canonical domain type.
One owning regular-file entry inside an .eva archive.
Admission budgets applied before allocating decoded archive entries.
Fully admitted source archive with its typed manifest and payloads.
std::vector< EvaArchiveEntry > entries
Validated, owning .eva manifest.