Files
EVEngine/test/asset_import.cpp

244 lines
11 KiB
C++

#include "asset/AssetCooker.h"
#include "asset/RuntimeDefinition.h"
#include "asset/EvpackResourceReader.h"
#include "asset/graphics/EvpackGraphicsLoader.h"
#include "asset/graphics/EvpackImageLoader.h"
#include "asset/import/AssetImporter.h"
#include "graphics/IMeshResourceFactory.h"
#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include <algorithm>
#include <fstream>
#include <iterator>
using namespace eve;
using namespace eve::asset;
using namespace eve::asset_import;
namespace {
PersistentId importId() {
return *PersistentId::parse("018f6f22-2490-7ad2-bf58-4f1dbca31040");
}
ImportPackageIdentity identity(std::string name) {
return {importId(), std::move(name), "1.0.0",
{{"provider", Value("local")},
{"license", Value(Value::Object{{"redistribution", Value("project-only")}})}}};
}
std::vector<std::uint8_t> fileBytes(const std::string& path) {
std::ifstream input(path, std::ios::binary);
return {std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
}
const EvaArchiveEntry* entry(const PreparedAssetImport& prepared, std::string_view path) {
const auto found = std::find_if(prepared.entries.begin(), prepared.entries.end(),
[&](const auto& value) { return value.path == path; });
return found == prepared.entries.end() ? nullptr : &*found;
}
AssetCookProfile linuxProfile() {
return {{"linux", "x86_64", "vulkan", {"rgba8"}, "spirv-1.6", "high", {}},
CookPublication::LocalInspection, 16};
}
class VerticalMeshFactory final : public eve::graphics::IMeshResourceFactory {
public:
Result<eve::graphics::Mesh*> uploadMesh(const float*, const float*, const float*,
int vertices, const std::uint32_t*,
int indices) override {
vertexCount = vertices;
indexCount = indices;
return Result<eve::graphics::Mesh*>::success(
reinterpret_cast<eve::graphics::Mesh*>(&token));
}
Result<void> releaseMesh(eve::graphics::Mesh*) override { return Result<void>::success(); }
int vertexCount = 0;
int indexCount = 0;
private:
std::uint64_t token = 0;
};
class VerticalImageFactory final : public eve::graphics::IImageResourceFactory {
public:
Result<eve::graphics::Texture*> uploadRgba8(std::uint32_t widthValue,
std::uint32_t heightValue,
const std::uint8_t* pixels,
bool srgbValue) override {
width = widthValue;
height = heightValue;
srgb = srgbValue;
firstPixel.assign(pixels, pixels + 4);
return Result<eve::graphics::Texture*>::success(
reinterpret_cast<eve::graphics::Texture*>(&token));
}
Result<void> releaseImage(eve::graphics::Texture*) override {
return Result<void>::success();
}
std::uint32_t width = 0;
std::uint32_t height = 0;
bool srgb = true;
std::vector<std::uint8_t> firstPixel;
private:
std::uint64_t token = 0;
};
std::vector<uint8_t> pixelPng() {
return {0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00,
0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4, 0x89, 0x00, 0x00, 0x00,
0x0d, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x63, 0xf8, 0xcf, 0xc0, 0xf0, 0x1f, 0x00, 0x05, 0x00, 0x01, 0xff,
0x72, 0x9c, 0x52, 0x67, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82};
}
} // namespace
TEST_CASE("asset.import.pngTraversesEvaCookAndEvpack") {
// Complete 1x1 RGBA PNG. The source is retained losslessly in canonical eve.image.
const auto png = pixelPng();
auto corruptPng = png;
corruptPng[45] ^= 1;
auto corrupt = prepareImageImport({identity("image.corrupt"), "corrupt.png", corruptPng,
ImageColorSpace::Srgb, "color", {}});
REQUIRE(!corrupt.ok());
CHECK_EQ(corrupt.error()->code(), DiagnosticCode::HashMismatch);
auto prepared = prepareImageImport({identity("image.test"), "pixel.png", png,
ImageColorSpace::Srgb, "color", {}});
REQUIRE(prepared.ok());
REQUIRE_EQ(prepared.value().manifest.assets.size(), std::size_t(1));
REQUIRE(entry(prepared.value(), "reports/import.json") != nullptr);
CHECK_EQ(prepared.value().sourceMappings.size(), std::size_t(1));
CHECK(prepared.value().manifest.provenance.contains("importKey"));
auto evaBytes = buildEvaArchive(prepared.value().manifest, prepared.value().entries);
REQUIRE(evaBytes.ok());
auto eva = parseEvaArchive(evaBytes.value());
REQUIRE(eva.ok());
auto cooked = cookEvaToEvpack(eva.value(), linuxProfile());
REQUIRE(cooked.ok());
auto runtime = parseEvpack(cooked.value().bytes);
REQUIRE(runtime.ok());
CHECK_EQ(runtime.value().chunks().size(), std::size_t(2));
auto runtimeDefinition = runtime.value().decodeChunk(0, 4096);
auto runtimePixels = runtime.value().decodeChunk(1, 4096);
REQUIRE(runtimeDefinition.ok());
REQUIRE(runtimePixels.ok());
auto definition = decodeRuntimeDefinition(runtimeDefinition.value());
REQUIRE(definition.ok());
const auto* definitionObject = definition.value().getIf<Value::Object>();
REQUIRE(definitionObject != nullptr);
CHECK_EQ(definitionObject->at("encoding"), Value("rgba8"));
REQUIRE_EQ(runtimePixels.value().size(), std::size_t(28));
CHECK_EQ(std::string(reinterpret_cast<const char*>(runtimePixels.value().data()), 5),
std::string("EVIMG"));
auto admitted = std::make_shared<const Evpack>(std::move(runtime).takeValue());
EvpackResourceReader reader(admitted);
VerticalImageFactory imageFactory;
eve::asset_graphics::EvpackImageLoader imageLoader(reader, imageFactory);
auto uploaded = imageLoader.load(
prepared.value().manifest.assets.front().asset,
{"linux", "x86_64", "vulkan", {"rgba8"}, {"spirv-1.6"}, {"high"}, {}});
REQUIRE(uploaded.ok());
CHECK(uploaded.value().texture != nullptr);
CHECK_EQ(imageFactory.width, std::uint32_t(1));
CHECK_EQ(imageFactory.height, std::uint32_t(1));
CHECK(!imageFactory.srgb);
CHECK_EQ(imageFactory.firstPixel.size(), std::size_t(4));
auto unsupportedProfile = linuxProfile();
unsupportedProfile.variant.textureFamilies = {"bc"};
auto unsupported = cookEvaToEvpack(eva.value(), unsupportedProfile);
REQUIRE(!unsupported.ok());
CHECK_EQ(unsupported.error()->code(), DiagnosticCode::Unsupported);
}
TEST_CASE("asset.import.realGlbDecodesCanonicalMeshAndTraversesRuntimePack") {
const std::string path = "test/assets/housegen/kenney-modular-buildings/building-block.glb";
auto bytes = fileBytes(path);
REQUIRE(!bytes.empty());
GltfImportRequest request{identity("gltf.test"), "building-block.glb", std::move(bytes), {}, {}};
// The geometry fixture omits its external colormap. Supply an explicit test image;
// this verifies mesh/package traversal, not the original material appearance.
request.externalResources["Textures/colormap.png"] = pixelPng();
auto prepared = prepareGltfImport(request);
REQUIRE(prepared.ok());
REQUIRE(!prepared.value().manifest.assets.empty());
REQUIRE(entry(prepared.value(), "reports/import.json") != nullptr);
const auto meshCount =
std::count_if(prepared.value().manifest.assets.begin(), prepared.value().manifest.assets.end(),
[](const auto& a) { return a.type == "eve.mesh"; });
REQUIRE_EQ(prepared.value().sourceMappings.size(), size_t(meshCount));
CHECK_EQ(prepared.value().manifest.assets.front().type, std::string("eve.mesh"));
auto evaBytes = buildEvaArchive(prepared.value().manifest, prepared.value().entries);
REQUIRE(evaBytes.ok());
auto eva = parseEvaArchive(evaBytes.value());
REQUIRE(eva.ok());
auto cooked = cookEvaToEvpack(eva.value(), linuxProfile());
REQUIRE(cooked.ok());
auto runtime = parseEvpack(cooked.value().bytes);
REQUIRE(runtime.ok());
bool foundMesh = false;
for (size_t i = 0; i < runtime.value().chunks().size(); ++i) {
const auto& chunk = runtime.value().chunks()[i];
if (chunk.type != "eve.mesh" || chunk.kind != EvpackChunkKind::Bulk) continue;
auto meshBlob = runtime.value().decodeChunk(i, chunk.decodedSize);
REQUIRE(meshBlob.ok());
REQUIRE(meshBlob.value().size() >= 8);
REQUIRE_EQ(std::string(reinterpret_cast<const char*>(meshBlob.value().data()), 6), std::string("EVMESH"));
foundMesh = true;
}
REQUIRE(foundMesh);
const auto entrypoint = prepared.value().manifest.entrypoints.find("default");
REQUIRE(entrypoint != prepared.value().manifest.entrypoints.end());
auto admitted = std::make_shared<const Evpack>(std::move(runtime).takeValue());
EvpackResourceReader reader(admitted);
VerticalMeshFactory factory;
eve::asset_graphics::EvpackGraphicsLoader loader(reader, factory);
auto uploaded = loader.loadMesh(
entrypoint->second,
{"linux", "x86_64", "vulkan", {"rgba8"}, {"spirv-1.6"}, {"high"}, {}});
REQUIRE(uploaded.ok());
CHECK(uploaded.value().mesh != nullptr);
CHECK(factory.vertexCount > 0);
CHECK(factory.indexCount > 0);
}
TEST_CASE("asset.import.gltfRejectsNetworkAndTraversalUris") {
const std::string json = R"json({"asset":{"version":"2.0"},"buffers":[
{"uri":"../outside.bin","byteLength":4}],"bufferViews":[],"accessors":[],"meshes":[]})json";
GltfImportRequest request{identity("gltf.unsafe"), "unsafe.gltf",
{json.begin(), json.end()}, {}, {}};
auto rejected = prepareGltfImport(request);
REQUIRE(!rejected.ok());
CHECK_EQ(rejected.error()->code(), DiagnosticCode::Unsupported);
}
TEST_CASE("asset.import.tamperedStructuredReportIsRejectedBeforePublication") {
const std::vector<std::uint8_t> png = {
0x89,0x50,0x4e,0x47,0x0d,0x0a,0x1a,0x0a,0x00,0x00,0x00,0x0d,0x49,0x48,0x44,0x52,
0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x01,0x08,0x06,0x00,0x00,0x00,0x1f,0x15,0xc4,
0x89,0x00,0x00,0x00,0x0d,0x49,0x44,0x41,0x54,0x08,0xd7,0x63,0xf8,0xcf,0xc0,0xf0,
0x1f,0x00,0x05,0x00,0x01,0xff,0x72,0x9c,0x52,0x67,0x00,0x00,0x00,0x00,0x49,0x45,
0x4e,0x44,0xae,0x42,0x60,0x82};
auto prepared = prepareImageImport({identity("image.report-security"), "pixel.png", png,
ImageColorSpace::Srgb, "color", {}});
REQUIRE(prepared.ok());
auto report = std::find_if(prepared.value().entries.begin(), prepared.value().entries.end(),
[](const auto& value) { return value.path == "reports/import.json"; });
REQUIRE(report != prepared.value().entries.end());
std::string reportText(report->bytes.begin(), report->bytes.end());
const auto source = reportText.find("\"sourceEngine\":\"image\"");
REQUIRE(source != std::string::npos);
reportText.replace(source, std::string("\"sourceEngine\":\"image\"").size(),
"\"sourceEngine\":\"evil!\"");
report->bytes.assign(reportText.begin(), reportText.end());
auto rejected = buildEvaArchive(prepared.value().manifest, prepared.value().entries);
REQUIRE(!rejected.ok());
CHECK_EQ(rejected.error()->code(), DiagnosticCode::HashMismatch);
}