feat(file): allow case-insensitive file paths to be resolved on Linux

This commit is contained in:
phaneron 2023-12-04 17:16:20 -05:00
parent 58c8a50e8f
commit 3cfddf0149
6 changed files with 359 additions and 51 deletions

View file

@ -3,6 +3,7 @@ file(GLOB BC_SOURCES
"lock/*.cpp" "lock/*.cpp"
"os/*.cpp" "os/*.cpp"
"file/*.cpp" "file/*.cpp"
"file/path/*.cpp"
"time/*.cpp" "time/*.cpp"
"system/*.cpp" "system/*.cpp"
"system/file/*.cpp" "system/file/*.cpp"

View file

@ -1,51 +1 @@
#ifndef BC_FILE_PATH_HPP #include "bc/file/path/Path.hpp"
#define BC_FILE_PATH_HPP
#include "bc/file/Defines.hpp"
#include <cstddef>
namespace Blizzard {
namespace File {
namespace Path {
// Classes
// Quick native path translation on the stack.
// Can be slow in some instances, as it resorts to heap allocation if path size >= BC_FILE_MAX_PATH
// Note: this name is an invention, however its behavior is repeated in all System_File implementations.
// So it probably did exist as an inlined class.
class QuickNative {
private:
size_t size;
// stack allocation
char fastpath[BC_FILE_MAX_PATH];
// heap
char* fatpath;
public:
QuickNative(const char* path);
~QuickNative();
const char* Str();
size_t Size();
};
// Functions
void ForceTrailingSeparator(char* buf, size_t bufMax, char sep);
bool MakeBackslashPath(const char* path, char* result, size_t capacity);
bool MakeConsistentPath(const char* path, char* result, size_t capacity);
bool MakeNativePath(const char* path, char* result, size_t capacity);
bool MakeUnivPath(const char* path, char* result, size_t capacity);
bool MakeWindowsPath(const char* path, char* result, size_t capacity);
} // namespace Path
} // namespace File
} // namespace Blizzard
#endif

225
bc/file/path/Path.cpp Normal file
View file

@ -0,0 +1,225 @@
#include "bc/file/path/Path.hpp"
#include "bc/file/path/Posix.hpp"
#include "bc/String.hpp"
#include "bc/Memory.hpp"
#include "bc/Debug.hpp"
#include <cctype>
namespace Blizzard {
namespace File {
namespace Path {
// Classes
QuickNative::QuickNative(const char* path) {
this->size = 0;
this->slowpath = nullptr;
this->fastpath[0] = '\0';
if (!path) {
return;
}
// Null byte
constexpr size_t reserved = 1;
this->size = String::Length(path) + reserved;
char* nativePath = nullptr;
if (this->size < BC_FILE_MAX_PATH) {
// (fast)
nativePath = this->fastpath;
MakeNativePath(path, this->fastpath, BC_FILE_MAX_PATH);
} else {
// (slow)
this->slowpath = reinterpret_cast<char*>(Memory::Allocate(this->size));
nativePath = this->slowpath;
MakeNativePath(path, this->slowpath, this->size);
}
#if defined(WHOA_SYSTEM_LINUX)
// Linux typically does not involve a case-sensitive filesystem
// For compatibility, try to find if this path exists in another case
// This is much slower
// The size of the resolved path. Maybe be larger due to "./"
size_t resolvedSize = this->size + 2;
auto resolvedPath = static_cast<char*>(Memory::Allocate(resolvedSize));
resolvedPath[resolvedSize-1] = '\0';
bool wasResolved = ResolvePosixCasePath(nativepath, resolvedPath);
if (!wasResolved) {
Memory::Free(resolvedPath);
return;
}
if (this->slowpath != nullptr) {
Memory::Free(this->slowpath);
}
this->slowpath = resolvedPath;
#endif
}
QuickNative::~QuickNative() {
if (this->slowpath != nullptr) {
Memory::Free(this->slowpath);
}
}
const char* QuickNative::Str() {
if (this->slowpath != nullptr) {
return this->slowpath;
}
return this->fastpath;
}
size_t QuickNative::Size() {
return this->size;
}
// Functions
void ForceTrailingSeparator(char* buf, size_t bufMax, char sep) {
if (buf == nullptr) {
return;
}
// If no separator character is provided, infer correct separator based on runes
if (sep == '\0') {
auto ptr = buf;
char ch;
while (true) {
ch = *ptr++;
// Make inference based on what data we have.
if (ch == '/' || ch == '\\') {
sep = ch;
break;
}
if (ch == '\0') {
// Fail safe to the system's path separator.
sep = BC_FILE_SYSTEM_PATH_SEPARATOR;
break;
}
}
}
// Buffer needs at least two characters.
if (bufMax < 2) {
return;
}
// Slice off the end of the path buffer, so that any existing* trailing separator is discarded.
auto len = String::Length(buf);
char ch = '\0';
if (len > 0) {
ch = buf[len - 1];
}
switch (ch) {
case '/':
case '\\':
len--;
break;
default:
break;
}
// Add (back*) trailing separator
BLIZZARD_ASSERT(len <= bufMax - 2);
buf[len] = sep;
buf[len + 1] = '\0'; // pad null
}
// Convert path to DOS-style.
// the function will iterate through the path string, converting all occurrences of forward slashes (/) to backslashes (\)
bool MakeBackslashPath(const char* path, char* result, size_t capacity) {
size_t i = 0;
size_t c = capacity - 1;
while (i <= c) {
if (path[i] == '\0') {
result[i] = '\0';
return true;
}
result[i] = path[i] == '/' ? '\\' : path[i];
i++;
}
result[0] = '\0';
return false;
}
// Make a path string consistent before the last path separator.
bool MakeConsistentPath(const char* path, char* result, size_t capacity) {
if (!result || (capacity < 1)) {
return false;
}
if (!path || (capacity == 1)) {
*result = '\0';
return false;
}
for (auto i = capacity - 1; i != -1; i--) {
auto ch = path[i];
switch (ch) {
case '\\':
return MakeBackslashPath(path, result, capacity);
case '/':
return MakeUnivPath(path, result, capacity);
case '\0':
default:
break;
}
}
String::Copy(result, path, capacity);
return true;
}
// Convert any path string into something that can be used on the current OS.
bool MakeNativePath(const char* path, char* result, size_t capacity) {
#if defined(WHOA_SYSTEM_WIN)
return MakeWindowsPath(path, result, capacity);
#else
return MakeUnivPath(path, result, capacity);
#endif
}
// Convert a path string into something UNIX-friendly.
bool MakeUnivPath(const char* path, char* result, size_t capacity) {
size_t i = 0; // path and result index
size_t c = capacity - 1; // ceiling
while (i <= c) {
if (path[i] == '\0') {
result[i] = '\0';
return true;
}
result[i] = path[i] == '\\' ? '/' : path[i];
i++;
}
result[0] = '\0';
return false;
}
bool MakeWindowsPath(const char* path, char* result, size_t capacity) {
return MakeBackslashPath(path, result, capacity);
}
} // namespace Path
} // namespace File
} // namespace Blizzard

51
bc/file/path/Path.hpp Normal file
View file

@ -0,0 +1,51 @@
#ifndef BC_FILE_PATH_PATH_HPP
#define BC_FILE_PATH_PATH_HPP
#include "bc/file/Defines.hpp"
#include <cstddef>
namespace Blizzard {
namespace File {
namespace Path {
// Classes
// Quick native path translation on the stack.
// Can be slow in some instances, as it resorts to heap allocation if path size >= BC_FILE_MAX_PATH
// Note: this name is an invention, however its behavior is repeated in all System_File implementations.
// So it probably did exist as an inlined class.
class QuickNative {
private:
size_t size;
// stack allocation
char fastpath[BC_FILE_MAX_PATH];
// heap
char* slowpath;
public:
QuickNative(const char* path);
~QuickNative();
const char* Str();
size_t Size();
};
// Functions
void ForceTrailingSeparator(char* buf, size_t bufMax, char sep);
bool MakeBackslashPath(const char* path, char* result, size_t capacity);
bool MakeConsistentPath(const char* path, char* result, size_t capacity);
bool MakeNativePath(const char* path, char* result, size_t capacity);
bool MakeUnivPath(const char* path, char* result, size_t capacity);
bool MakeWindowsPath(const char* path, char* result, size_t capacity);
} // namespace Path
} // namespace File
} // namespace Blizzard
#endif

75
bc/file/path/Posix.cpp Normal file
View file

@ -0,0 +1,75 @@
#include "bc/file/path/Posix.hpp"
#if defined(WHOA_SYSTEM_LINUX)
#include <cstring>
#include <dirent.h>
#include <alloca.h>
#include <cstdio>
#include <cstdint>
// adapted from https://github.com/OneSadCookie/fcaseopen/blob/master/fcaseopen.c
bool ResolvePosixCasePath(const char* path, char* r) {
auto l = strlen(path);
auto p = static_cast<char*>(alloca(l + 1));
strcpy(p, path);
size_t rl = 0;
DIR* d;
if (p[0] == '/') {
d = opendir("/");
p = p + 1;
} else {
d = opendir(".");
r[0] = '.';
r[1] = 0;
rl = 1;
}
int32_t last = 0;
auto c = strsep(&p, "/");
while (c) {
if (!d) {
return false;
}
if (last) {
closedir(d);
return false;
}
r[rl] = '/';
rl += 1;
r[rl] = 0;
struct dirent* e = readdir(d);
while (e) {
if (strcasecmp(c, e->d_name) == 0) {
strcpy(r + rl, e->d_name);
rl += strlen(e->d_name);
closedir(d);
d = opendir(r);
break;
}
e = readdir(d);
}
if (!e) {
strcpy(r + rl, c);
rl += strlen(c);
last = 1;
}
c = strsep(&p, "/");
}
if (d) {
closedir(d);
}
return true;
}
#endif

6
bc/file/path/Posix.hpp Normal file
View file

@ -0,0 +1,6 @@
#ifndef BC_FILE_PATH_POSIX_HPP
#define BC_FILE_PATH_POSIX_HPP
bool ResolvePosixCasePath(const char* path, char* r);
#endif