Files
Destrum/destrum/src/FS/AssetFS.cpp
T
2026-10-22 00:30:23 +02:00

236 lines
8.3 KiB
C++

#include <destrum/FS/AssetFS.h>
#include <filesystem>
#include <fstream>
#include <stdexcept>
#include <string>
#include "spdlog/spdlog.h"
namespace {
struct ParsedVirtualPath {
std::string scheme;
std::filesystem::path relativePath;
};
[[nodiscard]] std::filesystem::path ValidateRelativePath(std::string_view value) {
if (value.empty()) {
throw std::runtime_error("asset path is empty");
}
const std::filesystem::path path{std::string(value)};
if (path.empty() || path.is_absolute() || path.has_root_path()) {
throw std::runtime_error("asset path must be relative");
}
for (const auto& part : path) {
if (part == "..") {
throw std::runtime_error("asset path traversal is not allowed");
}
}
const std::filesystem::path normalized = path.lexically_normal();
if (normalized.empty() || normalized == ".") {
throw std::runtime_error("asset path is invalid");
}
return normalized;
}
[[nodiscard]] ParsedVirtualPath ParseVirtualPath(std::string_view value) {
const std::size_t separator = value.find("://");
if (separator == std::string_view::npos || separator == 0) {
throw std::runtime_error("asset path must use scheme://relative/path syntax");
}
const std::string scheme(value.substr(0, separator));
if (scheme.find_first_of("/\\:") != std::string::npos) {
throw std::runtime_error("asset scheme is invalid");
}
return {
scheme,
ValidateRelativePath(value.substr(separator + 3))
};
}
[[nodiscard]] std::filesystem::path ResolvePath(const FSMount& mount,
const std::filesystem::path& relativePath) {
const std::filesystem::path root = mount.root.lexically_normal();
const std::filesystem::path full = (root / relativePath).lexically_normal();
const std::filesystem::path relative = full.lexically_relative(root);
if (relative.empty() || relative.is_absolute() || relative == ".." ||
relative.generic_string().starts_with("../")) {
throw std::runtime_error("asset path escapes its mount");
}
std::error_code error;
const std::filesystem::path canonicalRoot = std::filesystem::weakly_canonical(root, error);
if (error) {
throw std::runtime_error("failed to resolve asset mount: " + root.string());
}
error.clear();
const std::filesystem::path canonicalFull = std::filesystem::weakly_canonical(full, error);
if (error) {
throw std::runtime_error("failed to resolve asset path: " + full.string());
}
const std::filesystem::path canonicalRelative =
canonicalFull.lexically_relative(canonicalRoot);
if (canonicalRelative.empty() || canonicalRelative.is_absolute() ||
canonicalRelative == ".." || canonicalRelative.generic_string().starts_with("../")) {
throw std::runtime_error("asset path escapes its mount");
}
return full;
}
[[nodiscard]] const FSMount* FindMount(const std::vector<FSMount>& mounts,
const std::string& scheme) {
for (const FSMount& mount : mounts) {
if (mount.scheme == scheme) {
return &mount;
}
}
return nullptr;
}
}
void AssetFS::Init(std::filesystem::path exeDir) {
if (initialized) {
return;
}
const std::size_t originalMountCount = mounts.size();
try {
Mount("engine", exeDir / "assets/engine");
Mount("game", exeDir / "assets/game");
initialized = true;
} catch (...) {
mounts.resize(originalMountCount);
throw;
}
}
void AssetFS::Reset() {
mounts.clear();
initialized = false;
}
void AssetFS::Mount(std::string scheme, std::filesystem::path root) {
if (scheme.empty() || scheme.find_first_of("/\\:") != std::string::npos) {
throw std::runtime_error("asset mount scheme is invalid");
}
if (FindMount(mounts, scheme) != nullptr) {
throw std::runtime_error("asset mount already exists: " + scheme);
}
FSMount mount;
mount.scheme = std::move(scheme);
mount.root = std::filesystem::absolute(std::move(root)).lexically_normal();
const std::filesystem::path manifestPath = mount.root / "manifest.json";
std::error_code error;
if (std::filesystem::is_regular_file(manifestPath, error)) {
mount.manifest = FS::LoadAssetManifest(manifestPath);
}
spdlog::debug("Mounting assetfs scheme '{}' to root '{}'", mount.scheme, mount.root.string());
mounts.push_back(std::move(mount));
}
std::vector<uint8_t> AssetFS::ReadBytes(std::string_view vpath) {
return ReadFile(GetFullPath(vpath));
}
std::filesystem::path AssetFS::GetFullPath(std::string_view vpath) const {
if (!initialized) {
throw std::runtime_error("AssetFS is not initialized");
}
const ParsedVirtualPath parsed = ParseVirtualPath(vpath);
const FSMount* mount = FindMount(mounts, parsed.scheme);
if (mount == nullptr) {
throw std::runtime_error("asset mount not found: " + parsed.scheme);
}
return ResolvePath(*mount, parsed.relativePath);
}
std::filesystem::path AssetFS::GetCookedPathForFile(std::string_view vpath) const {
if (!initialized) {
throw std::runtime_error("AssetFS is not initialized");
}
const ParsedVirtualPath parsed = ParseVirtualPath(vpath);
const FSMount* mount = FindMount(mounts, parsed.scheme);
if (mount == nullptr) {
throw std::runtime_error("asset mount not found: " + parsed.scheme);
}
const std::filesystem::path rawPath = ResolvePath(*mount, parsed.relativePath);
if (mount->manifest) {
if (const ManifestAsset* asset = mount->manifest->FindBySrc(parsed.relativePath.generic_string())) {
if (asset->out) {
const std::filesystem::path cookedRelative = ValidateRelativePath(*asset->out);
const std::filesystem::path cookedPath = ResolvePath(*mount, cookedRelative);
std::error_code error;
if (std::filesystem::is_regular_file(cookedPath, error)) {
error.clear();
const auto sourceTime = std::filesystem::last_write_time(rawPath, error);
error.clear();
const auto cookedTime = std::filesystem::last_write_time(cookedPath, error);
if (!error && sourceTime > cookedTime) {
throw std::runtime_error(
"cooked asset output is stale: " + cookedPath.string());
}
return cookedPath;
}
throw std::runtime_error(
"cooked asset output is missing: " + cookedPath.string());
}
}
}
// Assets without a cooked output, such as shader includes, use the source path.
return rawPath;
}
std::optional<std::string> AssetFS::ToVirtualPath(const std::filesystem::path& fullPath) const {
const std::filesystem::path normalized = fullPath.lexically_normal();
for (const FSMount& mount : mounts) {
const std::filesystem::path relative = normalized.lexically_relative(mount.root);
if (relative.empty() || relative.is_absolute() || relative == "." ||
relative == ".." || relative.generic_string().starts_with("../")) {
continue;
}
return mount.scheme + "://" + relative.generic_string();
}
return std::nullopt;
}
std::vector<uint8_t> AssetFS::ReadFile(const std::filesystem::path& fullPath) {
std::ifstream file(fullPath, std::ios::binary);
if (!file) {
throw std::runtime_error("failed to open file: " + fullPath.string());
}
file.seekg(0, std::ios::end);
const std::streamsize size = file.tellg();
if (size < 0) {
throw std::runtime_error("failed to determine file size: " + fullPath.string());
}
file.seekg(0, std::ios::beg);
std::vector<uint8_t> buffer(static_cast<std::size_t>(size));
if (size > 0 && !file.read(reinterpret_cast<char*>(buffer.data()), size)) {
throw std::runtime_error("failed to read file: " + fullPath.string());
}
return buffer;
}