Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Path resolving performance improvements. #197

Merged
merged 10 commits into from
Sep 18, 2024
78 changes: 65 additions & 13 deletions cleo_sdk/CLEO_Utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
#include "CLEO.h"
#include "CPools.h" // from GTA Plugin SDK
#include "shellapi.h" // game window minimize/maximize support
#include <algorithm>
#include <filesystem>
#include <string>
#include <string_view>
#include <vector>
#include <wtypes.h>

Expand Down Expand Up @@ -76,8 +78,8 @@ namespace CLEO
OPCODE_WRITE_PARAM_PTR(value) // memory address
*/

static const char* Gta_Root_Dir_Path = (char*)0x00B71AE0;
static const char* Gta_User_Dir_Path = (char*)0x00C92368;
static const char* Gta_Root_Dir_Path = (char*)0x00B71AE0; // contains trailing path separator!
static const char* Gta_User_Dir_Path = (char*)0x00C92368; // no trailing separator

static bool IsLegacyScript(CLEO::CRunningScript* thread)
{
Expand Down Expand Up @@ -112,34 +114,84 @@ namespace CLEO
return result;
}

static bool StringStartsWith(const std::string_view str, const std::string_view prefix, bool caseSensitive = true)
{
if (str.length() < prefix.length())
{
return false;
}

if (caseSensitive)
{
return strncmp(str.data(), prefix.data(), prefix.length()) == 0;
}
else
{
return _strnicmp(str.data(), prefix.data(), prefix.length()) == 0;
}
}

static std::string ScriptInfoStr(CLEO::CRunningScript* thread)
{
std::string info(1024, '\0');
CLEO_GetScriptInfoStr(thread, true, info.data(), info.length());
return std::move(info);
}

// does file path points inside game directories? (game root or user files)
// Normalize filepath, collapse all parent directory references. Input should be absolute path without expandable %variables%
static void NormalizeFilepath(std::string& path, bool normalizeCase = true)
{
if (path.empty()) return;

std::replace(path.begin(), path.end(), '/', '\\');
if (normalizeCase) std::transform(path.begin(), path.end(), path.begin(), [](unsigned char c) { return tolower(c); }); // to lower case

// collapse references to parent directory
const auto ParentRef = "\\..\\";
const auto ParentRefLen = 4;

size_t refPos = path.find(ParentRef);
while (refPos != std::string::npos && refPos > 0)
{
size_t parentPos = path.rfind('\\', refPos - 1); // find start of parent name

if (parentPos == std::string::npos)
return; // parent must be root of the path then. We want to keep absolute path, let it be as is (even if "C:\..\" makes no sense)

path.replace(parentPos, (refPos - parentPos) + ParentRefLen - 1, ""); // remove parent and parent reference

refPos = path.find(ParentRef); // find next
}

while(path.back() == '\\') path.pop_back(); // remove trailing path separator(s)
}

// does normalized file path points inside game directories? (game root or user files)
static bool IsFilepathSafe(CLEO::CRunningScript* thread, const char* path)
{
auto IsSubpath = [](std::filesystem::path path, std::filesystem::path base)
if (strchr(path, '%') != nullptr)
{
auto relative = std::filesystem::relative(path, base);
return !relative.empty() && *relative.begin() != "..";
};
return false; // do not allow paths containing expandable variables
}

auto fsPath = std::filesystem::path(path);
if (!fsPath.is_absolute())
std::string absolute;
if (!std::filesystem::path(path).is_absolute())
{
fsPath = CLEO_GetScriptWorkDir(thread) / fsPath;
absolute = CLEO_GetScriptWorkDir(thread);
absolute += '\\';
absolute += path;
NormalizeFilepath(absolute, false);
path = absolute.c_str();
}

if (IsSubpath(fsPath, Gta_Root_Dir_Path) || IsSubpath(fsPath, Gta_User_Dir_Path))
// check prefix
if (!StringStartsWith(path, std::string_view(Gta_Root_Dir_Path, strlen(Gta_Root_Dir_Path) - 1), false) && // without ending separator
x87 marked this conversation as resolved.
Show resolved Hide resolved
!StringStartsWith(path, Gta_User_Dir_Path, false))
{
return true;
return false;
}

return false;
return true;
}

static bool IsObjectHandleValid(DWORD handle)
Expand Down
108 changes: 48 additions & 60 deletions source/CScriptEngine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -684,79 +684,67 @@ namespace CLEO
return {};
}

try
auto fsPath = FS::path(path);

// check for virtual path root
enum class VPref{ None, Game, User, Script, Cleo, Modules } virtualPrefix = VPref::None;
if(!fsPath.empty())
{
auto fsPath = FS::path(path);
const auto root = fsPath.begin()->string(); // first path element
const auto r = root.c_str();

// check for virtual path root
enum class VPref{ None, Game, User, Script, Cleo, Modules } virtualPrefix = VPref::None;
auto root = fsPath.begin();
if(root != fsPath.end())
{
if(*root == DIR_GAME) virtualPrefix = VPref::Game;
else if (*root == DIR_USER) virtualPrefix = VPref::User;
else if (*root == DIR_SCRIPT) virtualPrefix = VPref::Script;
else if (*root == DIR_CLEO) virtualPrefix = VPref::Cleo;
else if (*root == DIR_MODULES) virtualPrefix = VPref::Modules;
}
if(_strcmpi(r, DIR_GAME) == 0) virtualPrefix = VPref::Game;
else if (_strcmpi(r, DIR_USER) == 0) virtualPrefix = VPref::User;
else if (_strcmpi(r, DIR_SCRIPT) == 0) virtualPrefix = VPref::Script;
else if (_strcmpi(r, DIR_CLEO) == 0) virtualPrefix = VPref::Cleo;
else if (_strcmpi(r, DIR_MODULES) == 0) virtualPrefix = VPref::Modules;
}

// not virtual
if(virtualPrefix == VPref::None)
// not virtual
if(virtualPrefix == VPref::None)
{
if(fsPath.is_relative())
{
if(fsPath.is_relative())
{
if(customWorkDir != nullptr)
fsPath = ResolvePath(customWorkDir) / fsPath;
else
fsPath = GetWorkDir() / fsPath;

auto resolved = FS::weakly_canonical(fsPath).string();

// ModLoader support: do not expand game dir relative paths
if (resolved.find(Filepath_Root) == 0)
return FS::relative(resolved, Filepath_Root).string();
else
return resolved;
}

return FS::weakly_canonical(fsPath).string();
if(customWorkDir != nullptr)
fsPath = ResolvePath(customWorkDir) / fsPath;
else
fsPath = GetWorkDir() / fsPath;
}

// expand virtual paths
FS::path resolved;
auto result = fsPath.string();
NormalizeFilepath(result, false);

if (virtualPrefix == VPref::User) // user files location
// ModLoader support: keep game dir relative paths relative
if (result.length() > Filepath_Root.length() && // and separator
x87 marked this conversation as resolved.
Show resolved Hide resolved
result[Filepath_Root.length()] == '\\' && // path separator after game path
StringStartsWith(GetWorkDir(), Filepath_Root, false) && // curent work dir is game root
StringStartsWith(result, Filepath_Root, false)) // resulting path is within game root
{
resolved = GetUserDirectory();
result.replace(0, Filepath_Root.length() + 1, ""); // remove game root path prefix
}
else
if (virtualPrefix == VPref::Script) // this script's source file location
{
resolved = GetScriptFileDir();
}
else
{
// all remaing variants starts with game root
resolved = Filepath_Root;

switch(virtualPrefix)
{
case(VPref::Cleo): resolved /= "cleo"; break;
case(VPref::Modules): resolved /= "cleo\\cleo_modules"; break;
}
}

// append all but virtual prefix from original path
for(auto it = ++fsPath.begin(); it != fsPath.end(); it++)
resolved /= *it;

return FS::weakly_canonical(resolved).string(); // collapse "..\" uses
return std::move(result);
}
catch (const std::exception& ex)

// expand virtual paths
FS::path resolved;
switch(virtualPrefix)
{
TRACE("Error while resolving path: %s", ex.what());
return {};
case VPref::User: resolved = Gta_User_Dir_Path; break;
case VPref::Script: resolved = GetScriptFileDir(); break;
case VPref::Game: resolved = Filepath_Root; break;
case VPref::Cleo: resolved = FS::path(Filepath_Root) / "cleo"; break;
case VPref::Modules: resolved = FS::path(Filepath_Root) / "cleo\\modules"; break;
default : resolved = "<error>"; break; // should never happen
}

// append all but virtual prefix from original path
for (auto it = ++fsPath.begin(); it != fsPath.end(); it++)
resolved /= *it;

auto result = resolved.string();
NormalizeFilepath(result, false);
return std::move(result);
}

std::string CCustomScript::GetInfoStr(bool currLineInfo) const
Expand Down