mirror of
https://github.com/Jous99/F4MP.git
synced 2026-01-13 00:20:54 +01:00
301 lines
8.5 KiB
C++
301 lines
8.5 KiB
C++
|
|
// 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
|