F4MP/tiltedcode/Code/components/resources/ResourceCollection.cpp

301 lines
8.5 KiB
C++
Raw Normal View History

// Copyright (C) 2022 TiltedPhoques SRL.
// For licensing information see LICENSE at the root of this distribution.
#include "ResourceCollection.h"
#include "SemanticVersion2.h"
#include <base/Check.h>
#include <base/simpleini/SimpleIni.h>
#include <regex>
namespace Resources
{
namespace
{
constexpr char kResourceFolderName[] = "resources";
constexpr char kResourceManifestExt[] = ".manifest";
static const SemanticVersion kApiSet(1, 0, 0);
TiltedPhoques::Vector<TiltedPhoques::String> ParseQuotedTokens(const TiltedPhoques::String& aString)
{
TiltedPhoques::Vector<TiltedPhoques::String> 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<Resources::Manifest001>();
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<TiltedPhoques::String>
{
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<std::filesystem::path> 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