Skip to content

Commit 4321fd3

Browse files
authored
Merge pull request #1187 from asarium/fix/1182_2
Show error message when a data directory uses the wrong case
2 parents d40cf7a + ec82cb1 commit 4321fd3

File tree

12 files changed

+194
-30
lines changed

12 files changed

+194
-30
lines changed

code/cfile/cfilesystem.cpp

Lines changed: 131 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,25 @@
1515
#include <errno.h>
1616
#include <sstream>
1717
#include <algorithm>
18+
#include <memory>
1819

1920
#ifdef _WIN32
2021
#include <io.h>
2122
#include <direct.h>
2223
#include <windows.h>
2324
#include <winbase.h> /* needed for memory mapping of file functions */
25+
#include <shlwapi.h>
26+
27+
struct dir_handle_deleter {
28+
typedef HANDLE pointer;
29+
30+
void operator()(HANDLE dirp) {
31+
if (dirp != INVALID_HANDLE_VALUE) {
32+
FindClose(dirp);
33+
}
34+
}
35+
};
36+
typedef std::unique_ptr<HANDLE, dir_handle_deleter> unique_dir_handle_ptr;
2437
#endif
2538

2639
#ifdef SCP_UNIX
@@ -30,6 +43,14 @@
3043
#include <fnmatch.h>
3144
#include <sys/stat.h>
3245
#include <unistd.h>
46+
#include <libgen.h>
47+
48+
struct dir_deleter {
49+
void operator()(DIR* dirp) {
50+
closedir(dirp);
51+
}
52+
};
53+
typedef std::unique_ptr<DIR, dir_deleter> unique_dir_ptr;
3354
#endif
3455

3556
#include "cfile/cfile.h"
@@ -38,6 +59,7 @@
3859
#include "globalincs/pstypes.h"
3960
#include "localization/localize.h"
4061
#include "osapi/osapi.h"
62+
#include "parse/parselo.h"
4163

4264
#define CF_ROOTTYPE_PATH 0
4365
#define CF_ROOTTYPE_PACK 1
@@ -552,6 +574,62 @@ void cf_search_root_path(int root_index)
552574
}
553575

554576
#if defined _WIN32
577+
{
578+
// Check if the case matches the case as specified in Pathtypes
579+
// Since Windows paths are case insensitive this wouldn't cause issues here but other
580+
// platforms would fail to find data paths in this case so we show a nice error if we detect that here
581+
582+
// Ignore the root since the case of that is allowed to differ (it's handled in the mod handling)
583+
if (i != CF_TYPE_ROOT) {
584+
// We use FindFirstFileNameW for this, it should hopefully work...
585+
// First, convert our path from ASCII/UTF-8 to wchar_t
586+
std::string search_string = search_path;
587+
// Remove any trailing directory separators
588+
if (search_string[search_string.size() - 1] == '\\') {
589+
search_string = search_string.substr(0, search_string.size() - 1);
590+
}
591+
592+
char parent_name[MAX_PATH];
593+
memset(parent_name, 0, sizeof(parent_name));
594+
strcpy_s(parent_name, search_string.c_str());
595+
596+
CHAR file_name[MAX_PATH];
597+
memset(file_name, 0, sizeof(file_name));
598+
strcpy_s(file_name, search_string.c_str());
599+
600+
PathStripPathA(file_name);
601+
if (PathRemoveFileSpecA(parent_name)) {
602+
strcat_s(parent_name, "\\*");
603+
604+
WIN32_FIND_DATAA find_data;
605+
auto handle = unique_dir_handle_ptr(FindFirstFileA(parent_name, &find_data));
606+
if (handle.get() != INVALID_HANDLE_VALUE) {
607+
do {
608+
if (stricmp(find_data.cFileName, file_name)) {
609+
// Not the same name, not even if we check case-insensitive
610+
continue;
611+
}
612+
613+
// Same name, might have case differences
614+
if (!strcmp(find_data.cFileName, file_name)) {
615+
// Case matches, everything is alright.
616+
continue;
617+
}
618+
619+
// We need to do some formatting on the parent_name in order to show a nice error message
620+
SCP_string parent_name_str = parent_name;
621+
parent_name_str = parent_name_str.substr(0, parent_name_str.size() - 1); // Remove trailing *
622+
parent_name_str += find_data.cFileName;
623+
624+
// If we are still here then the case didn't match which means that we have to show the error message
625+
Error(LOCATION, "Data directory '%s' for path type '%s' does not match the required case! "
626+
"All data directories must exactly match the case specified by the engine or your mod "
627+
"will not work on other platforms.", parent_name_str.c_str(), Pathtypes[i].path);
628+
} while (FindNextFileA(handle.get(), &find_data) != 0);
629+
}
630+
}
631+
}
632+
}
555633
strcat_s( search_path, "*.*" );
556634

557635
intptr_t find_handle;
@@ -588,21 +666,65 @@ void cf_search_root_path(int root_index)
588666
_findclose( find_handle );
589667
}
590668
#elif defined SCP_UNIX
591-
DIR *dirp;
592-
struct dirent *dir;
669+
auto dirp = unique_dir_ptr(opendir(search_path));
670+
if (!dirp)
671+
{
672+
// If the directory does not exist then check if it might exist with a different case. If that's the case
673+
// we bail out with an error so inform the user that this is not valid.
674+
675+
// On Unix we can have a different case for the search paths so we also need to account for that
676+
// We do that by looking at the parent of search_path and enumerating all directories and then check if any of
677+
// them are a case-insensitive match
678+
char dirname_copy[CF_MAX_PATHNAME_LENGTH];
679+
memcpy(dirname_copy, search_path, sizeof(search_path));
680+
// According to the documentation of directory_name and basename, the return value does not need to be freed
681+
auto directory_name = dirname(dirname_copy);
682+
683+
char basename_copy[CF_MAX_PATHNAME_LENGTH];
684+
memcpy(basename_copy, search_path, sizeof(search_path));
685+
// According to the documentation of dirname and basename, the return value does not need to be freed
686+
auto search_name = basename(basename_copy);
687+
688+
auto parentDirP = unique_dir_ptr(opendir(directory_name));
689+
if (parentDirP) {
690+
struct dirent *dir = nullptr;
691+
while ((dir = readdir (parentDirP.get())) != nullptr) {
692+
693+
if (stricmp(search_name, dir->d_name)) {
694+
continue;
695+
}
696+
697+
SCP_string fn;
698+
sprintf(fn, "%s/%s", directory_name, dir->d_name);
699+
700+
struct stat buf;
701+
if (stat(fn.c_str(), &buf) == -1) {
702+
continue;
703+
}
593704

594-
dirp = opendir (search_path);
595-
if ( dirp ) {
596-
while ((dir = readdir (dirp)) != NULL)
705+
if (S_ISDIR(buf.st_mode)) {
706+
// Found a case insensitive match
707+
// If the name is not exactly the same as the one we are currently searching then it's an error
708+
if (strcmp(search_name, dir->d_name)) {
709+
Error(LOCATION, "Data directory '%s' for path type '%s' does not match the required case! "
710+
"All data directories must exactly match the case specified by the engine or your mod "
711+
"will not work on other platforms.", fn.c_str(), Pathtypes[i].path);
712+
}
713+
break;
714+
}
715+
}
716+
}
717+
} else {
718+
struct dirent *dir = nullptr;
719+
while ((dir = readdir (dirp.get())) != NULL)
597720
{
598721
if (!fnmatch ("*.*", dir->d_name, 0))
599722
{
600-
char fn[MAX_PATH];
601-
snprintf(fn, MAX_PATH-1, "%s%s", search_path, dir->d_name);
602-
fn[MAX_PATH-1] = 0;
723+
SCP_string fn;
724+
sprintf(fn, "%s%s", search_path, dir->d_name);
603725

604726
struct stat buf;
605-
if (stat(fn, &buf) == -1) {
727+
if (stat(fn.c_str(), &buf) == -1) {
606728
continue;
607729
}
608730

@@ -632,7 +754,6 @@ void cf_search_root_path(int root_index)
632754
}
633755
}
634756
}
635-
closedir(dirp);
636757
}
637758
#endif
638759
}

test/src/cfile/cfile.cpp

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
2+
#include <gtest/gtest.h>
3+
#include <graphics/font.h>
4+
5+
#include "util/FSTestFixture.h"
6+
7+
class CFileTest : public test::FSTestFixture {
8+
public:
9+
CFileTest() : test::FSTestFixture(INIT_NONE) {
10+
pushModDir("cfile");
11+
}
12+
13+
protected:
14+
virtual void SetUp() override {
15+
test::FSTestFixture::SetUp();
16+
}
17+
virtual void TearDown() override {
18+
test::FSTestFixture::TearDown();
19+
20+
cfile_close();
21+
}
22+
};
23+
24+
25+
TEST_F(CFileTest, wrong_data_case) {
26+
SCP_string cfile_dir(TEST_DATA_PATH);
27+
cfile_dir += DIR_SEPARATOR_CHAR;
28+
cfile_dir += "test"; // Cfile expects something after the path
29+
30+
ASSERT_THROW(cfile_init(cfile_dir.c_str()), os::dialogs::ErrorException);
31+
}

test/src/menuui/test_intel_parse.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
class IntelParseTest : public test::FSTestFixture {
99
public:
10-
IntelParseTest() : test::FSTestFixture(0) {
10+
IntelParseTest() : test::FSTestFixture(INIT_CFILE) {
1111
pushModDir("menuui");
1212
pushModDir("intel_parse");
1313
}

test/src/parse/test_parselo.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
class ParseloTest : public test::FSTestFixture {
99
public:
10-
ParseloTest() : test::FSTestFixture(0) {
10+
ParseloTest() : test::FSTestFixture(INIT_CFILE) {
1111
pushModDir("parselo");
1212
}
1313

@@ -162,4 +162,4 @@ TEST(ParseloUtilTest, drop_whitespace) {
162162
test_str = " ";
163163
drop_white_space(test_str);
164164
ASSERT_EQ(test_str, "");
165-
}
165+
}

test/src/scripting/ScriptingTestFixture.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ extern "C" {
1111
namespace test {
1212
namespace scripting {
1313

14-
ScriptingTestFixture::ScriptingTestFixture(uint64_t init_flags) : FSTestFixture(init_flags) {
14+
ScriptingTestFixture::ScriptingTestFixture(uint64_t init_flags) : FSTestFixture(init_flags | INIT_CFILE) {
1515
pushModDir("scripting");
1616
}
1717
void ScriptingTestFixture::SetUp() {

test/src/scripting/api/base.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
class ScriptingBaseTest : public test::scripting::ScriptingTestFixture {
55
public:
6-
ScriptingBaseTest() : test::scripting::ScriptingTestFixture(0) {
6+
ScriptingBaseTest() : test::scripting::ScriptingTestFixture(INIT_NONE) {
77
pushModDir("base");
88
}
99
};

test/src/scripting/api/bitops.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
class BitOpsTest : public test::scripting::ScriptingTestFixture {
55
public:
6-
BitOpsTest() : test::scripting::ScriptingTestFixture(0) {
6+
BitOpsTest() : test::scripting::ScriptingTestFixture(INIT_NONE) {
77
pushModDir("bitops");
88
}
99
};
@@ -30,4 +30,4 @@ TEST_F(BitOpsTest, checkBit) {
3030

3131
TEST_F(BitOpsTest, addBit) {
3232
this->EvalTestScript();
33-
}
33+
}

test/src/scripting/api/enums.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
class EnumsTest : public test::scripting::ScriptingTestFixture {
55
public:
6-
EnumsTest() : test::scripting::ScriptingTestFixture(0) {
6+
EnumsTest() : test::scripting::ScriptingTestFixture(INIT_NONE) {
77
pushModDir("enums");
88
}
99
};

test/src/source_groups.cmake

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ add_file_folder(root ""
1212
test_stubs.cpp
1313
)
1414

15+
add_file_folder(cfile "cfile"
16+
cfile/cfile.cpp
17+
)
18+
1519
add_file_folder(graphics "Globalincs"
1620
globalincs/test_flagset.cpp
1721
globalincs/test_safe_strings.cpp

test/src/util/FSTestFixture.cpp

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ void test::FSTestFixture::SetUp() {
3030

3131
os_init("Test", "Test");
3232

33+
if (!(_initFlags & INIT_CFILE)) {
34+
return;
35+
}
36+
37+
// The rest depends on cfile initialization
3338
SCP_string cfile_dir(TEST_DATA_PATH);
3439
cfile_dir += DIR_SEPARATOR_CHAR;
3540
cfile_dir += "test"; // Cfile expects something after the path
@@ -52,19 +57,21 @@ void test::FSTestFixture::SetUp() {
5257
}
5358
}
5459
void test::FSTestFixture::TearDown() {
55-
if (_initFlags & INIT_SHIPS) {
56-
ship_close();
57-
}
60+
if (_initFlags & INIT_CFILE) {
61+
if (_initFlags & INIT_SHIPS) {
62+
ship_close();
63+
}
5864

59-
if (_initFlags & INIT_GRAPHICS) {
60-
io::mouse::CursorManager::shutdown();
65+
if (_initFlags & INIT_GRAPHICS) {
66+
io::mouse::CursorManager::shutdown();
6167

62-
bm_unload_all();
68+
bm_unload_all();
6369

64-
gr_close();
65-
}
70+
gr_close();
71+
}
6672

67-
cfile_close();
73+
cfile_close();
74+
}
6875

6976
timer_close();
7077

0 commit comments

Comments
 (0)