// Copyright (C) 2022 TiltedPhoques SRL. // For licensing information see LICENSE at the root of this distribution. #include "ResourceCollection.h" #include "SemanticVersion2.h" #include #include #include namespace Resources { namespace { constexpr char kResourceFolderName[] = "resources"; constexpr char kResourceManifestExt[] = ".manifest"; static const SemanticVersion kApiSet(1, 0, 0); TiltedPhoques::Vector ParseQuotedTokens(const TiltedPhoques::String& aString) { TiltedPhoques::Vector result; size_t start = aString.find_first_of('"'); while (start != std::string::npos) { size_t end = aString.find_first_of('"', start + 1); if (end == std::string::npos) { break; } result.push_back(aString.substr(start + 1, end - start - 1)); start = aString.find_first_of('"', end + 1); while (start != std::string::npos && isspace(aString[start])) { ++start; } } return result; } Manifest001::DependencyTuple SplitDependencyString(const TiltedPhoques::String& aString) { size_t atPos = aString.find_first_of('@'); if (atPos == TiltedPhoques::String::npos) { return {aString, {0, 0, 0}}; } auto name = aString.substr(0, atPos); auto versionString = aString.substr(atPos + 1); SemanticVersion version(versionString.c_str()); return {name, version}; } TiltedPhoques::String UnescapeAndStrip(const std::string_view& aString) { TiltedPhoques::String result; for (char c : aString) { if (c == '\\') { continue; } else if (c == '"') { continue; } else if (c == '\n') { continue; } result += c; } return result; } // Returns true if the given string is a valid semantic version. bool IsValidSemanticVersion(const std::string& acVersion) { // Use a regular expression to match the semantic version format: // major.minor.patch // where major, minor, and patch are non-negative integers. std::regex pattern("^\\d+\\.\\d+\\.\\d+$"); return std::regex_match(acVersion, pattern); } } // namespace ResourceCollection::ResourceCollection() { CollectResources(); } ResourceCollection::~ResourceCollection() { } bool ResourceCollection::LoadManifestData(const std::filesystem::path& aPath) { // example: // name = "my-resource" // resource_version = 1.0.0 // apiset = 1.0.0 // description = "A resource that does stuff" // keywords = ["selfmade", "resource"] // license = "MIT" // repository = "" // homepage = "" // entrypoint = "test.lua" // dependencies = ["dependency-a@1.0.0", "dependency-b@1.0.0"] CSimpleIni ini; { auto buf = TiltedPhoques::LoadFile(aPath); if (ini.LoadData(buf.c_str()) != SI_Error::SI_OK) { spdlog::error("Failed to load manifest file {}", aPath.string()); return false; } } auto manifest = TiltedPhoques::MakeUnique(); manifest->FolderName = aPath.parent_path().filename(); // version data auto readSemVer = [&](const char* apName) -> SemanticVersion { const char* pValue = ini.GetValue("Resource", apName, nullptr); if (pValue == nullptr) { spdlog::error("Missing key \"{}\" in {}", apName, aPath.string()); return ""; } if (!IsValidSemanticVersion(pValue)) { spdlog::error("Invalid semantic version for key \"{}\" in {}", apName, aPath.string()); return {0, 0, 0}; } return SemanticVersion(pValue); }; manifest->ApiSet = readSemVer("apiset"); manifest->ResourceVersion = readSemVer("version"); if (!manifest->ApiSet || !manifest->ResourceVersion) { return false; } if (manifest->ApiSet > Resources::kApiSet) { spdlog::error("Resource {} requires a newer API set than the current one", aPath.string()); return false; } auto readString = [&](const char* apName) -> TiltedPhoques::String { const char* pValue = ini.GetValue("Resource", apName, nullptr); if (pValue == nullptr) { spdlog::error("Missing key \"{}\" in {}", apName, aPath.string()); return ""; } return UnescapeAndStrip(pValue); }; auto readStringOptional = [&](const char* apName) -> TiltedPhoques::String { const char* pValue = ini.GetValue("Resource", apName, nullptr); if (pValue == nullptr) { return ""; } return UnescapeAndStrip(pValue); }; // read must haves manifest->Name = readString("name"); manifest->Description = readString("description"); manifest->EntryPoint = readStringOptional("entrypoint"); if (manifest->Name.empty() || manifest->Description.empty()) { return false; } // optional entries manifest->License = readStringOptional("license"); manifest->Repository = readStringOptional("repository"); manifest->Homepage = readStringOptional("homepage"); // lists of strings auto readStringList = [&ini](const char* apName) -> TiltedPhoques::Vector { return ParseQuotedTokens(ini.GetValue("Resource", apName, "")); }; for (const auto& dep : readStringList("dependencies")) { manifest->Dependencies.push_back(SplitDependencyString(dep)); } m_manifests.push_back(std::move(manifest)); return true; } void ResourceCollection::ResolveDependencies() { for (auto& manifest : m_manifests) { for (const auto& dep : manifest->Dependencies) { bool wasFound = false; for (auto& it2 : m_manifests) { if (it2->Name == dep.Name) { if (!dep.Version) { spdlog::error("Resource \"{}\": Dependency {} version is invalid", manifest->Name, dep.Name); manifest->IsTombstone = true; continue; } else if (dep.Version != it2->ResourceVersion) { spdlog::error("Resource \"{}\": Dependency {} has version {} but {} is required", manifest->Name, dep.Name, SemVerToString(it2->ResourceVersion), SemVerToString(dep.Version)); manifest->IsTombstone = true; } // let the loop continue to list all other conflicts immedeatly. wasFound = true; } } if (!wasFound) { spdlog::error("Resource \"{}\": Dependency {} not found", manifest->Name, dep.Name); manifest->IsTombstone = true; } } } } void ResourceCollection::CollectResources() { m_resourcePath = std::filesystem::current_path() / kResourceFolderName; if (!std::filesystem::exists(m_resourcePath)) { std::filesystem::create_directory(m_resourcePath); } TiltedPhoques::Vector manifestCanidates; for (const auto& packageFolder : std::filesystem::directory_iterator(m_resourcePath)) { if (!packageFolder.is_directory()) continue; for (const auto& packageContent : std::filesystem::directory_iterator(packageFolder)) { if (packageContent.is_directory()) continue; if (packageContent.path().extension() == kResourceManifestExt) { manifestCanidates.push_back(packageContent.path()); } } } uint32_t failedCount = 0; for (const auto& path : manifestCanidates) { if (!LoadManifestData(path)) { failedCount++; } } if (failedCount > 0) { spdlog::warn("{} manifests failed to load", failedCount); } ResolveDependencies(); failedCount = 0; for (auto iter = m_manifests.begin(); iter != m_manifests.end();) { if ((*iter)->IsTombstone) { failedCount++; iter = m_manifests.erase(iter); } else ++iter; } if (failedCount > 0) { spdlog::warn("{} resources failed to load", failedCount); } spdlog::info("Loaded {} resources", m_manifests.size()); } } // namespace Resources