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

'Mass file loader' for use in skin and asset loading #6727

Closed
wants to merge 28 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
096897c
initial file loader
ewancg Jun 9, 2023
e961fb8
flesh out description, minor fixes
ewancg Jun 9, 2023
5021136
small fixes
ewancg Jun 9, 2023
646d526
that's not why
ewancg Jun 9, 2023
76db8df
move all error checking to load step, remove fs_is_readable
ewancg Jun 10, 2023
babd456
change std fs symlink detection to lstat, std fs api not on macos <10.15
ewancg Jun 10, 2023
74d1152
struct stat not stat
ewancg Jun 10, 2023
b88e8ea
hopefully satisfy clang-format and clang-tidy
ewancg Jun 10, 2023
b7e89d7
hopefully satisfy clang-format
ewancg Jun 10, 2023
3d47803
fix header guard
ewancg Jun 10, 2023
e8e1ed7
hopefully satisfy clang-format
ewancg Jun 10, 2023
9b5aac1
turn not init error into runtime assert
ewancg Jun 14, 2023
21fef42
break dbg_assert at file loader impl load into multiple checks
ewancg Jun 16, 2023
5114f76
basic async impl, move from /game/client to /engine/shared, fixes as …
ewancg Jul 29, 2023
1617774
forgot to add that
ewancg Jul 29, 2023
0c65fd5
hopefully please dev warnings
ewancg Jul 29, 2023
3c4a970
add flag for skip bom
ewancg Aug 25, 2023
98334ce
merge implementations, get rid of the pseudo-interface nonsense, ebb …
ewancg Aug 26, 2023
1a44575
remove symlink functionality
ewancg Aug 26, 2023
3276b1c
use job instead of raw thread
ewancg Aug 26, 2023
eb0a28f
cleanup
ewancg Aug 26, 2023
d7cb3ba
add lockfree thread communication
ewancg Aug 27, 2023
e795af7
remove a few more std string
ewancg Aug 27, 2023
18d21ce
cleanup
ewancg Aug 27, 2023
a071edd
cleanup
ewancg Aug 27, 2023
2e2bfca
fix crash on shutdown where cskins dtor expects valid ptr from Graphi…
ewancg Aug 28, 2023
ab44dbc
wtf
ewancg Aug 28, 2023
2d8c571
maybe smarter mechanism for handling job state
ewancg Aug 28, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 3 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -730,7 +730,7 @@ endif()

if(TARGET_OS STREQUAL "windows")
set(PLATFORM_CLIENT)
set(PLATFORM_CLIENT_LIBS opengl32 winmm imm32)
set(PLATFORM_CLIENT_LIBS NtosKrnl Ntdll opengl32 winmm imm32)
set(PLATFORM_LIBS)
list(APPEND PLATFORM_LIBS shlwapi) # PathIsRelativeW
list(APPEND PLATFORM_LIBS version ws2_32) # Windows sockets
Expand Down Expand Up @@ -1941,6 +1941,8 @@ set_src(ENGINE_SHARED GLOB_RECURSE src/engine/shared
engine.cpp
fifo.cpp
fifo.h
file_loader.cpp
file_loader.h
filecollection.cpp
filecollection.h
global_uuid_manager.cpp
Expand Down
2 changes: 2 additions & 0 deletions src/base/system.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <cstdarg>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <iterator> // std::size
#include <string_view>

Expand Down Expand Up @@ -81,6 +82,7 @@
#include <shlobj.h> // SHChangeNotify
#include <shlwapi.h>
#include <wincrypt.h>
#include <winioctl.h> // FSCTL_GET_REPARSE_POINT
#else
#error NOT IMPLEMENTED
#endif
Expand Down
53 changes: 53 additions & 0 deletions src/engine/client/client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@
#include <chrono>
#include <thread>

// test: remove
#include <engine/shared/file_loader.h>
// end test

using namespace std::chrono_literals;

static const ColorRGBA gs_ClientNetworkPrintColor{0.7f, 1, 0.7f, 1.0f};
Expand Down Expand Up @@ -4451,6 +4455,55 @@ void CClient::RegisterCommands()
m_pConsole->Chain("gfx_borderless", ConchainWindowBordered, this);
m_pConsole->Chain("gfx_vsync", ConchainWindowVSync, this);

// test: remove
auto FileLoaderTest = [](IConsole::IResult *pResult, void *pUserData) {
CClient *client = reinterpret_cast<CClient *>(pUserData);
auto *FileLoader = new CMassFileLoader(client->m_pEngine, client->m_pStorage, CMassFileLoader::LOAD_FLAGS_ABSOLUTE_PATH | /*CMassFileLoader::LOAD_FLAGS_ASYNC |*/ CMassFileLoader::LOAD_FLAGS_RECURSE_SUBDIRECTORIES);

FileLoader->SetLoadFailedCallback([](CMassFileLoader::ELoadError Error, const void *pData, void *) -> bool {
char Message[128];
switch(Error)
{
case CMassFileLoader::LOAD_ERROR_INVALID_SEARCH_PATH:
str_format(Message, sizeof(Message), "Invalid path: '%s'", reinterpret_cast<const char *>(pData));
break;
case CMassFileLoader::LOAD_ERROR_FILE_UNREADABLE:
str_format(Message, sizeof(Message), "File unreadable: '%s'", reinterpret_cast<const char *>(pData));
break;
case CMassFileLoader::LOAD_ERROR_FILE_TOO_LARGE:
str_format(Message, sizeof(Message), "File too large: '%s'", reinterpret_cast<const char *>(pData));
break;
case CMassFileLoader::LOAD_ERROR_INVALID_EXTENSION:
str_format(Message, sizeof(Message), "Invalid extension: '%s'", reinterpret_cast<const char *>(pData));
break;
case CMassFileLoader::LOAD_ERROR_UNKNOWN:
[[fallthrough]];
default:
dbg_msg("test", "Unknown error. Continuing...");
return true;
}

dbg_msg("test", "%s", Message);
return true;
});

FileLoader->SetFileLoadedCallback([](const std::string_view
ItemName,
const unsigned char *pData, const unsigned int Size, void *) {
dbg_msg("test", "File of %d bytes loaded: '%s'", Size, ItemName.data());
unsigned char *pArray = reinterpret_cast<unsigned char *>(malloc(Size));
memcpy(pArray, pData, Size);
free(pArray);
});

FileLoader->SetPaths(":test");
// FileLoader->SetFileExtension("");
FileLoader->Load();
};

m_pConsole->Register("file_loader_test", "", CFGFLAG_CLIENT | CFGFLAG_STORE, FileLoaderTest, this, "Test");
// end test

// DDRace

#define CONSOLE_COMMAND(name, params, flags, callback, userdata, help) m_pConsole->Register(name, params, flags, 0, 0, help);
Expand Down
16 changes: 9 additions & 7 deletions src/engine/client/graphics_threaded.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -711,10 +711,11 @@ bool CGraphics_Threaded::CheckImageDivisibility(const char *pFileName, CImageInf
bool HeightBroken = Img.m_Height == 0 || (Img.m_Height % DivY) != 0;
if(WidthBroken || HeightBroken)
{
SWarning NewWarning;
str_format(NewWarning.m_aWarningMsg, sizeof(NewWarning.m_aWarningMsg), Localize("The width of texture %s is not divisible by %d, or the height is not divisible by %d, which might cause visual bugs."), pFileName, DivX, DivY);
// SWarning NewWarning;
// str_format(NewWarning.m_aWarningMsg, sizeof(NewWarning.m_aWarningMsg), Localize("The width of texture %s is not divisible by %d, or the height is not divisible by %d, which might cause visual bugs."), pFileName, DivX, DivY);

m_vWarnings.emplace_back(NewWarning);
// m_vWarnings.emplace_back(NewWarning);
dbg_msg("graphics", Localize("The width of texture %s is not divisible by %d, or the height is not divisible by %d, which might cause visual bugs."), pFileName, DivX, DivY);

ImageIsValid = false;
}
Expand Down Expand Up @@ -757,16 +758,17 @@ bool CGraphics_Threaded::IsImageFormatRGBA(const char *pFileName, CImageInfo &Im
{
if(Img.m_Format != CImageInfo::FORMAT_RGBA)
{
SWarning NewWarning;
// SWarning NewWarning;
char aText[128];
aText[0] = '\0';
if(pFileName)
{
str_format(aText, sizeof(aText), "\"%s\"", pFileName);
}
str_format(NewWarning.m_aWarningMsg, sizeof(NewWarning.m_aWarningMsg),
Localize("The format of texture %s is not RGBA which will cause visual bugs."), aText);
m_vWarnings.emplace_back(NewWarning);
// str_format(NewWarning.m_aWarningMsg, sizeof(NewWarning.m_aWarningMsg),
// Localize("The format of texture %s is not RGBA which will cause visual bugs."), aText);
// m_vWarnings.emplace_back(NewWarning);
dbg_msg("graphics", Localize("The format of texture %s is not RGBA which will cause visual bugs."), aText);
return false;
}
return true;
Expand Down
234 changes: 234 additions & 0 deletions src/engine/shared/file_loader.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
#include "file_loader.h"
#include <base/system.h>

CMassFileLoader::CMassFileLoader(IEngine *pEngine, IStorage *pStorage, uint8_t Flags)
{
m_pEngine = pEngine;
m_pStorage = pStorage;
m_Flags = Flags;
}

CMassFileLoader::~CMassFileLoader()
{
if(m_pExtension)
free(m_pExtension);
for(const auto &it : m_PathCollection)
{
delete it.second;
}
}

void CMassFileLoader::SetFileExtension(const std::string_view Extension)
{
m_pExtension = static_cast<char *>(malloc((Extension.length()) + 1 * sizeof(char)));
str_format(m_pExtension, Extension.length() + 1, "%s", Extension.data());
}

inline bool CMassFileLoader::CompareExtension(const std::filesystem::path &Filename, const std::string_view Extension)
{
// std::string is justified here because of std::transform, and because std::filesystem::path::c_str() will
// return const wchar_t *, but char width is handled automatically when using string
std::string FileExtension = Filename.extension().string();
std::transform(FileExtension.begin(), FileExtension.end(), FileExtension.begin(),
[](unsigned char c) { return std::tolower(c); });
return FileExtension == Extension; // Extension is already lowered
}

[[maybe_unused]] int CMassFileLoader::ListDirectoryCallback(const char *Name, int IsDir, int, void *User)
{
auto *pUserData = reinterpret_cast<SListDirectoryCallbackUserInfo *>(User);
if(pUserData->m_pThis->m_Continue)
{
auto *pFileList = pUserData->m_pThis->m_PathCollection.find(pUserData->m_pCurrentDirectory)->second;
char AbsolutePath[IO_MAX_PATH_LENGTH];
str_format(AbsolutePath, sizeof(AbsolutePath), "%s/%s", pUserData->m_pCurrentDirectory, Name);

if(!str_comp(Name, ".") || !str_comp(Name, ".."))
return 0;

if(!IsDir)
{
if((pUserData->m_pThis->m_pExtension == nullptr || str_comp(pUserData->m_pThis->m_pExtension, "")) || CompareExtension(Name, pUserData->m_pThis->m_pExtension))
pFileList->push_back(Name);
}
else if(pUserData->m_pThis->m_Flags & LOAD_FLAGS_RECURSE_SUBDIRECTORIES)
{
// Note that adding data to a SORTED container that is currently being iterated on higher in
// scope would invalidate the iterator. This is not sorted
pUserData->m_pThis->m_PathCollection.insert({AbsolutePath, new std::vector<std::string>});
SListDirectoryCallbackUserInfo Data{AbsolutePath, pUserData->m_pThis};

// Directory item is a directory, must be recursed
pUserData->m_pThis->m_pStorage->ListDirectory(IStorage::TYPE_ALL, AbsolutePath, ListDirectoryCallback, &Data);
}
}

return 0;
}

unsigned int CMassFileLoader::Begin(CMassFileLoader *pUserData)
{
char aPathBuffer[IO_MAX_PATH_LENGTH];
for(auto &It : pUserData->m_RequestedPaths)
{
if(pUserData->m_Continue)
{
int StorageType = It.find(':') == 0 ? IStorage::STORAGETYPE_BASIC : IStorage::STORAGETYPE_CLIENT;
if(StorageType == IStorage::STORAGETYPE_BASIC)
It.erase(0, 1);
pUserData->m_pStorage->GetCompletePath(StorageType, It.c_str(), aPathBuffer, sizeof(aPathBuffer));
if(fs_is_dir(aPathBuffer)) // Exists and is a directory
pUserData->m_PathCollection.insert({std::string(aPathBuffer), new std::vector<std::string>});
else
pUserData->m_Continue = TryCallback(pUserData->m_fnLoadFailedCallback, LOAD_ERROR_INVALID_SEARCH_PATH, It.c_str(), pUserData->m_pUser);
}
}

if(pUserData->m_pExtension && !str_comp(pUserData->m_pExtension, ""))
{
// must be .x at the shortest
if(str_length(pUserData->m_pExtension) == 1 || pUserData->m_pExtension[0] != '.')
pUserData->m_Continue = TryCallback(pUserData->m_fnLoadFailedCallback, LOAD_ERROR_INVALID_EXTENSION, pUserData->m_pExtension, pUserData->m_pUser);
for(int i = 0; i < str_length(pUserData->m_pExtension); i++)
pUserData->m_pExtension[i] = std::tolower(pUserData->m_pExtension[i]);
}

for(const auto &It : pUserData->m_PathCollection)
{
if(pUserData->m_Continue)
{
const char *Key = It.first.c_str();
SListDirectoryCallbackUserInfo Data{Key, pUserData};
pUserData->m_pStorage->ListDirectory(IStorage::TYPE_ALL, Key, ListDirectoryCallback, &Data);
}
}

// Index is now populated, load the files
unsigned char *pData = nullptr;
unsigned int Size, Count = 0;
IOHANDLE Handle;
for(const auto &Directory : pUserData->m_PathCollection)
{
for(const auto &File : *Directory.second)
{
if(pUserData->m_Continue)
{
// Wait for our turn
if(pUserData->m_Flags & LOAD_FLAGS_ASYNC)
{
bool Wait = true;
while(Wait)
{
switch(pUserData->GetJobStatus())
{
case CFileLoadJob::FILE_LOAD_JOB_STATUS_YIELD:
std::this_thread::yield();
break;
case CFileLoadJob::FILE_LOAD_JOB_STATUS_DONE:
pUserData->m_Continue = false;
[[fallthrough]];
default:
Wait = false;
break;
}
}
}

// Construct file path
char FilePath[IO_MAX_PATH_LENGTH];
str_format(FilePath, sizeof(FilePath), "%s/%s", Directory.first.c_str(), File.c_str());

// Return if dry run
if(pUserData->m_Flags & LOAD_FLAGS_DONT_READ_FILE)
{
pUserData->m_fnFileLoadedCallback(pUserData->m_Flags & LOAD_FLAGS_ABSOLUTE_PATH ? FilePath : File, nullptr, 0, pUserData->m_pUser);
Count++;
continue;
}

// Probe readability
Handle = io_open(FilePath, IOFLAG_READ | (pUserData->m_Flags & LOAD_FLAGS_SKIP_BOM ? IOFLAG_SKIP_BOM : 0));
if(!Handle)
{
pUserData->m_Continue = TryCallback(pUserData->m_fnLoadFailedCallback, LOAD_ERROR_FILE_UNREADABLE, FilePath, pUserData->m_pUser);
continue;
}

// Check size
// system.cpp APIs are only good up to 2 GiB at the moment (signed long cannot describe a size any larger)
io_seek(Handle, 0, IOSEEK_END);
long ExpectedSize = io_tell(Handle);
io_seek(Handle, 0, IOSEEK_START);
Comment on lines +158 to +161
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems redundant and like a TOCTOU bug. It's better to just read the entire file immediately with IStorage::ReadFile.

Copy link
Member

@Robyt3 Robyt3 Aug 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure whether it's a good idea to always load the files into memory though. Some of our loaders only work with files, like datafiles (maps).

Edit: Never mind, I saw there is a flag LOAD_FLAGS_DONT_READ_FILE for this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is a TOCTOU bug. This is a very common way to probe file size (and as far as I know, the only way using the libc like the system APIs here are doing). Unless you mean I shouldn't be checking the file size at all?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, there is no reason to check the size first. The io_read_all and ReadFile functions ensure that all that can be read is read.

Copy link
Contributor Author

@ewancg ewancg Aug 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I originally added this so I could consistently differentiate between empty files (where fread returns 0) and files that are too big to be read (where fread also returns 0 on some compilers). I guess it's not vital that this distinction is made, but it would certainly be nice. IDK of another way to do this without checking the size first.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does io_length return -1 for empty files? If so, that's a bug and should be fixed separately. If not, then only use io_read_all and remove ExpectedSize.

If you want to check for too large files (which is unnecessary in my opinion), then maybe you could check in io_read_all instead (in a separate PR).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It returns -1 for files larger than 2GB on some compilers.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see no reason for all this code tbh. It seems to just call io_read_all. If io_read_all has a bug, then that bug should be fixed, and not worked around.


// File is either too large for io_tell/ftell to say or it's actually empty (MinGW returns 0; MSVC returns -1)
if(ExpectedSize <= 0)
{
size_t RealSize = std::filesystem::file_size(FilePath);
if(static_cast<size_t>(ExpectedSize) != RealSize)
{
pUserData->m_Continue = TryCallback(pUserData->m_fnLoadFailedCallback, LOAD_ERROR_FILE_TOO_LARGE, FilePath, pUserData->m_pUser);
continue;
}
}

// Load file
io_read_all(Handle, reinterpret_cast<void **>(&pData), &Size);
if(static_cast<unsigned int>(ExpectedSize) != Size) // Possibly redundant, but accounts for memory allocation shortcomings and not just IO
{
pUserData->m_Continue = TryCallback(pUserData->m_fnLoadFailedCallback, LOAD_ERROR_FILE_TOO_LARGE, FilePath, pUserData->m_pUser);
continue;
}

// Return & cleanup
pUserData->m_fnFileLoadedCallback(pUserData->m_Flags & LOAD_FLAGS_ABSOLUTE_PATH ? FilePath : File, pData, Size, pUserData->m_pUser);
free(pData);
Count++;
io_close(Handle);
}
}
}

if(pUserData->m_Flags & LOAD_FLAGS_ASYNC)
{
if(pUserData->m_fnLoadFinishedCallback)
pUserData->m_fnLoadFinishedCallback(Count, pUserData->m_pUser);
pUserData->m_Finished = true;
return 0;
}
else
return Count;
}

#define MASS_FILE_LOADER_ERROR_PREFIX "Mass file loader used "
std::optional<unsigned int> CMassFileLoader::Load()
{
dbg_assert(!m_RequestedPaths.empty(), MASS_FILE_LOADER_ERROR_PREFIX "without adding paths."); // Ensure paths have been added
dbg_assert(bool(m_pStorage), MASS_FILE_LOADER_ERROR_PREFIX "without passing a valid IStorage instance."); // Ensure storage is valid
dbg_assert(bool(m_fnFileLoadedCallback), MASS_FILE_LOADER_ERROR_PREFIX "without implementing file loaded callback."); // Ensure file loaded callback is implemented
dbg_assert(m_Flags ^ LOAD_FLAGS_MASK, MASS_FILE_LOADER_ERROR_PREFIX "with invalid flags."); // Ensure flags are in bounds
dbg_assert(!m_Finished, MASS_FILE_LOADER_ERROR_PREFIX "after having already been used."); // Ensure is not reused
if(m_Flags & LOAD_FLAGS_ASYNC)
{
dbg_assert(bool(m_pEngine), MASS_FILE_LOADER_ERROR_PREFIX "without passing a valid IEngine instance."); // Ensure engine is valid
m_FileLoadJob = std::make_shared<CFileLoadJob>(&CMassFileLoader::Begin, this);
m_pEngine->AddJob(m_FileLoadJob);
return std::nullopt;
}
else
return Begin(this);
}

#undef MASS_FILE_LOADER_ERROR_PREFIX

/* TODO:
* [+] test error callback return value, make sure if false is returned the callback is never called again
* [ ] test every combination of flags
* [+,+] test symlink and readable detection on windows and unix
* ^ (waiting on working readable/symlink detection)
* [x] see if you can error check regex
*
* [ ] fix unreadable
*
* [+] test multiple directories
* [ ] async implementation
*/