From 6d367d4ab25407f9e1f3018028be01f35ac80176 Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Sat, 12 Sep 2026 09:27:14 +1000 Subject: [PATCH] [common/client] platform: make filesystem access portable --- client/platform/Linux/config.c | 60 ++++++ client/platform/Windows/config.c | 42 ++++ client/renderers/EGL/filter_glsl.c | 59 +++--- client/renderers/EGL/postprocess.c | 51 +++-- client/src/config.c | 75 ++----- client/src/config_platform.h | 31 +++ client/src/main.c | 5 +- client/src/util.c | 3 +- client/tests/CMakeLists.txt | 24 +++ client/tests/common_platform_test.c | 41 ++++ client/tests/config_platform_test.c | 92 +++++++++ common/include/common/filesystem.h | 50 +++++ common/src/option.c | 3 +- common/src/platform/linux/CMakeLists.txt | 1 + common/src/platform/linux/filesystem.c | 131 ++++++++++++ common/src/platform/windows/CMakeLists.txt | 1 + common/src/platform/windows/filesystem.c | 229 +++++++++++++++++++++ 17 files changed, 780 insertions(+), 118 deletions(-) create mode 100644 client/platform/Linux/config.c create mode 100644 client/platform/Windows/config.c create mode 100644 client/src/config_platform.h create mode 100644 client/tests/config_platform_test.c create mode 100644 common/include/common/filesystem.h create mode 100644 common/src/platform/linux/filesystem.c create mode 100644 common/src/platform/windows/filesystem.c diff --git a/client/platform/Linux/config.c b/client/platform/Linux/config.c new file mode 100644 index 00000000..d4e3ce36 --- /dev/null +++ b/client/platform/Linux/config.c @@ -0,0 +1,60 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "config_platform.h" + +#include "common/debug.h" +#include "common/paths.h" +#include "common/stringutils.h" + +#include +#include +#include + +static bool visitJoinedPath(const char * directory, const char * filename, + ConfigFileVisitor visitor, void * opaque) +{ + char * path; + if (alloc_sprintf(&path, "%s/%s", directory, filename) < 0) + { + DEBUG_ERROR("out of memory"); + return false; + } + + const bool result = visitor(path, opaque); + free(path); + return result; +} + +bool config_platformVisitDefaultFiles( + ConfigFileVisitor visitor, void * opaque) +{ + if (!visitor("/etc/looking-glass-client.ini", opaque)) + return false; + + const struct passwd * password = getpwuid(getuid()); + if (!password) + DEBUG_WARN("getpwuid failed, unable to load user configuration"); + else if (!visitJoinedPath(password->pw_dir, ".looking-glass-client.ini", + visitor, opaque)) + return false; + + return visitJoinedPath(lgConfigDir(), "client.ini", visitor, opaque); +} diff --git a/client/platform/Windows/config.c b/client/platform/Windows/config.c new file mode 100644 index 00000000..26bd7575 --- /dev/null +++ b/client/platform/Windows/config.c @@ -0,0 +1,42 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "config_platform.h" + +#include "common/debug.h" +#include "common/paths.h" +#include "common/stringutils.h" + +#include + +bool config_platformVisitDefaultFiles( + ConfigFileVisitor visitor, void * opaque) +{ + char * path; + if (alloc_sprintf(&path, "%s\\client.ini", lgConfigDir()) < 0) + { + DEBUG_ERROR("out of memory"); + return false; + } + + const bool result = visitor(path, opaque); + free(path); + return result; +} diff --git a/client/renderers/EGL/filter_glsl.c b/client/renderers/EGL/filter_glsl.c index b7605a8e..b9381743 100644 --- a/client/renderers/EGL/filter_glsl.c +++ b/client/renderers/EGL/filter_glsl.c @@ -24,6 +24,7 @@ #include "app.h" #include "common/debug.h" +#include "common/filesystem.h" #include "common/option.h" #include "common/paths.h" #include "common/stringlist.h" @@ -35,7 +36,6 @@ #include "basic.vert.h" #include -#include #include #include #include @@ -46,7 +46,6 @@ #include #include #include -#include #define GLSL_MAX_TEXTURES 16 #define GLSL_EXPR_STACK 128 @@ -1133,27 +1132,24 @@ static bool scanDirectory(const char * root, const char * relative, if (!path) return false; - DIR * directory = opendir(path); - if (!directory) + StringList entries = lgListDirectory(path); + if (!entries) { - DEBUG_ERROR("Unable to open GLSL shader directory '%s': %s", - path, strerror(errno)); + DEBUG_ERROR("Unable to open GLSL shader directory '%s'", path); free(path); return false; } bool result = true; - struct dirent * entry; - while ((entry = readdir(directory))) + for (unsigned int i = 0; i < stringlist_count(entries); ++i) { - if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) - continue; + const char * entry = stringlist_at(entries, i); char * childRelative; if (relative && *relative) - alloc_sprintf(&childRelative, "%s/%s", relative, entry->d_name); + alloc_sprintf(&childRelative, "%s/%s", relative, entry); else - childRelative = lg_strdup(entry->d_name); + childRelative = lg_strdup(entry); if (!childRelative) { result = false; @@ -1169,23 +1165,18 @@ static bool scanDirectory(const char * root, const char * relative, break; } - struct stat statbuf; - if (lstat(childPath, &statbuf) == 0) + const LGFileType type = lgPathTypeNoFollow(childPath); + if (type == LG_FILE_TYPE_DIRECTORY) { - if (S_ISDIR(statbuf.st_mode)) - { - result = scanDirectory(root, childRelative, files); - free(childRelative); - } - else if (S_ISREG(statbuf.st_mode) && hasGLSLExtension(entry->d_name)) - { - const unsigned int count = stringlist_count(files); - stringlist_push(files, childRelative); - result = stringlist_count(files) == count + 1; - if (!result) - free(childRelative); - } - else + result = scanDirectory(root, childRelative, files); + free(childRelative); + } + else if (type == LG_FILE_TYPE_REGULAR && hasGLSLExtension(entry)) + { + const unsigned int count = stringlist_count(files); + stringlist_push(files, childRelative); + result = stringlist_count(files) == count + 1; + if (!result) free(childRelative); } else @@ -1196,7 +1187,7 @@ static bool scanDirectory(const char * root, const char * relative, break; } - closedir(directory); + stringlist_free(&entries); free(path); return result; } @@ -1219,13 +1210,13 @@ static bool glslCreate(EGL_FilterAddFn add, void * opaque) return false; DEBUG_INFO("Scanning runtime GLSL filters in: %s", g_shaderPath); - struct stat statbuf; - if (stat(g_shaderPath, &statbuf) < 0 && (!configured || !*configured)) + if (lgPathType(g_shaderPath) == LG_FILE_TYPE_MISSING && + (!configured || !*configured)) { - if (mkdir(g_shaderPath, S_IRWXU) < 0 && errno != EEXIST) + if (!lgCreateDirectory(g_shaderPath)) { - DEBUG_ERROR("Unable to create GLSL shader directory '%s': %s", - g_shaderPath, strerror(errno)); + DEBUG_ERROR("Unable to create GLSL shader directory '%s'", + g_shaderPath); return false; } } diff --git a/client/renderers/EGL/postprocess.c b/client/renderers/EGL/postprocess.c index 3df60887..1f734228 100644 --- a/client/renderers/EGL/postprocess.c +++ b/client/renderers/EGL/postprocess.c @@ -24,15 +24,13 @@ #include "app.h" #include "cimgui.h" -#include #include #include -#include #include -#include #include "common/debug.h" #include "common/array.h" +#include "common/filesystem.h" #include "common/option.h" #include "common/paths.h" #include "common/stringlist.h" @@ -113,8 +111,6 @@ static void loadPreset(struct EGL_PostProcess * this, const char * name); static void loadPresetList(struct EGL_PostProcess * this) { - DIR * dir = NULL; - alloc_sprintf(&this->presetDir, "%s/presets", lgConfigDir()); if (!this->presetDir) { @@ -122,14 +118,14 @@ static void loadPresetList(struct EGL_PostProcess * this) return; } - if (mkdir(this->presetDir, S_IRWXU) < 0 && errno != EEXIST) + if (!lgCreateDirectory(this->presetDir)) { DEBUG_ERROR("Failed to create presets directory: %s", this->presetDir); goto fail; } - dir = opendir(this->presetDir); - if (!dir) + StringList entries = lgListDirectory(this->presetDir); + if (!entries) { DEBUG_ERROR("Failed to open presets directory: %s", this->presetDir); goto fail; @@ -139,33 +135,45 @@ static void loadPresetList(struct EGL_PostProcess * this) if (!this->presets) { DEBUG_ERROR("Failed to allocate memory for preset list"); - goto fail; + goto fail_entries; } - struct dirent * entry; const char * preset = option_get_string("egl", "preset"); this->activePreset = -1; - while ((entry = readdir(dir)) != NULL) + for (unsigned int i = 0; i < stringlist_count(entries); ++i) { - if (entry->d_type != DT_REG) + const char * entry = stringlist_at(entries, i); + char * path; + alloc_sprintf(&path, "%s/%s", this->presetDir, entry); + if (!path) + goto fail_entries; + + const LGFileType type = lgPathTypeNoFollow(path); + free(path); + if (type != LG_FILE_TYPE_REGULAR) continue; - DEBUG_INFO("Found preset: %s", entry->d_name); - char * name = strdup(entry->d_name); + DEBUG_INFO("Found preset: %s", entry); + char * name = strdup(entry); if (!name) { DEBUG_ERROR("Failed to allocate memory"); - goto fail; + goto fail_entries; } + const unsigned int count = stringlist_count(this->presets); stringlist_push(this->presets, name); + if (stringlist_count(this->presets) != count + 1) + { + free(name); + goto fail_entries; + } app_registerImGuiText(name); if (preset && strcmp(preset, name) == 0) this->activePreset = stringlist_count(this->presets) - 1; } - closedir(dir); - + stringlist_free(&entries); if (preset) { @@ -177,11 +185,12 @@ static void loadPresetList(struct EGL_PostProcess * this) return; +fail_entries: + stringlist_free(&entries); + fail: free(this->presetDir); this->presetDir = NULL; - if (dir) - closedir(dir); if (this->presets) stringlist_free(&this->presets); } @@ -222,7 +231,7 @@ static bool savePreset(struct EGL_PostProcess * this, const char * name) return false; } - FILE * file = fopen(path, "w"); + FILE * file = lgOpenFile(path, "w"); if (!file) { const char * strError = strerror(errno); @@ -362,7 +371,7 @@ static void deletePreset(struct EGL_PostProcess * this) return; } - unlink(path); + lgDeleteFile(path); free(path); stringlist_remove(this->presets, this->activePreset); if (this->activePreset >= stringlist_count(this->presets)) diff --git a/client/src/config.c b/client/src/config.c index f9eaaa80..56d92263 100644 --- a/client/src/config.c +++ b/client/src/config.c @@ -20,19 +20,17 @@ #include "main.h" #include "config.h" +#include "config_platform.h" #include "kb.h" -#include "common/option.h" #include "common/debug.h" -#include "common/paths.h" +#include "common/filesystem.h" +#include "common/option.h" #include "common/stringutils.h" #include #include -#include #include -#include -#include // forwards static bool optRendererParse (struct Option * opt, const char * str); @@ -565,62 +563,21 @@ void config_init(void) option_register(options); } +static bool loadDefaultConfig(const char * path, void * opaque) +{ + (void)opaque; + + if (lgPathType(path) != LG_FILE_TYPE_REGULAR) + return true; + + DEBUG_INFO("Loading config from: %s", path); + return option_load(path); +} + bool config_load(int argc, char * argv[]) { - // load any global options first - struct stat st; - if (stat("/etc/looking-glass-client.ini", &st) >= 0 && S_ISREG(st.st_mode)) - { - DEBUG_INFO("Loading config from: /etc/looking-glass-client.ini"); - if (!option_load("/etc/looking-glass-client.ini")) - return false; - } - - // load config from user's home directory - struct passwd * pw = getpwuid(getuid()); - if (!pw) - DEBUG_WARN("getpwuid failed, unable to load user configuration"); - else - { - char * localFile; - alloc_sprintf(&localFile, "%s/.looking-glass-client.ini", pw->pw_dir); - if (!localFile) - { - DEBUG_ERROR("out of memory"); - return false; - } - - if (stat(localFile, &st) >= 0 && S_ISREG(st.st_mode)) - { - DEBUG_INFO("Loading config from: %s", localFile); - if (!option_load(localFile)) - { - free(localFile); - return false; - } - } - free(localFile); - } - - // load config from XDG_CONFIG_HOME - char * xdgFile; - alloc_sprintf(&xdgFile, "%s/client.ini", lgConfigDir()); - if (!xdgFile) - { - DEBUG_ERROR("out of memory"); + if (!config_platformVisitDefaultFiles(loadDefaultConfig, NULL)) return false; - } - - if (xdgFile && stat(xdgFile, &st) >= 0 && S_ISREG(st.st_mode)) - { - DEBUG_INFO("Loading config from: %s", xdgFile); - if (!option_load(xdgFile)) - { - free(xdgFile); - return false; - } - } - free(xdgFile); // parse the command line arguments if (!option_parse(argc, argv)) @@ -630,7 +587,7 @@ bool config_load(int argc, char * argv[]) const char * configFile = option_get_string("app", "configFile"); if (configFile) { - if (stat(configFile, &st) < 0 || !S_ISREG(st.st_mode)) + if (lgPathType(configFile) != LG_FILE_TYPE_REGULAR) { DEBUG_ERROR("app:configFile set to invalid file: %s", configFile); return false; diff --git a/client/src/config_platform.h b/client/src/config_platform.h new file mode 100644 index 00000000..f8484f7e --- /dev/null +++ b/client/src/config_platform.h @@ -0,0 +1,31 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifndef _H_LG_CLIENT_CONFIG_PLATFORM_ +#define _H_LG_CLIENT_CONFIG_PLATFORM_ + +#include + +typedef bool (*ConfigFileVisitor)(const char * path, void * opaque); + +bool config_platformVisitDefaultFiles( + ConfigFileVisitor visitor, void * opaque); + +#endif diff --git a/client/src/main.c b/client/src/main.c index 5ebf9d18..31d7fc74 100644 --- a/client/src/main.c +++ b/client/src/main.c @@ -46,6 +46,7 @@ #include "common/thread.h" #include "common/locking.h" #include "common/event.h" +#include "common/filesystem.h" #include "common/time.h" #include "common/version.h" #include "common/paths.h" @@ -716,8 +717,8 @@ static void preSwapCallback(void * udata) .dataSize = capture.dataSize, }; - FILE * file = fopen(l_testCapture.path, "wb"); - bool written = false; + FILE * file = lgOpenFile(l_testCapture.path, "wb"); + bool written = false; if (file) { written = diff --git a/client/src/util.c b/client/src/util.c index 02b58ae1..400272cc 100644 --- a/client/src/util.c +++ b/client/src/util.c @@ -23,6 +23,7 @@ #include "font.h" #include "common/debug.h" +#include "common/filesystem.h" #include "common/stringutils.h" #include @@ -279,7 +280,7 @@ static ImFont * uiFontAddSize(ImFontAtlas * atlas, float size) bool util_fileGetContents(const char * filename, char ** buffer, size_t * length) { - FILE * fh = fopen(filename, "r"); + FILE * fh = lgOpenFile(filename, "r"); if (!fh) { DEBUG_ERROR("Failed to open the file: %s", filename); diff --git a/client/tests/CMakeLists.txt b/client/tests/CMakeLists.txt index 14253d00..f161b430 100644 --- a/client/tests/CMakeLists.txt +++ b/client/tests/CMakeLists.txt @@ -7,6 +7,9 @@ target_link_libraries(common-platform-tests ${EXE_FLAGS} lg_common ) +target_compile_definitions(common-platform-tests PRIVATE + LG_TEST_TMP_DIR="${CMAKE_CURRENT_BINARY_DIR}" +) add_test(NAME common-platform-tests COMMAND common-platform-tests ) @@ -14,6 +17,26 @@ set_tests_properties(common-platform-tests PROPERTIES TIMEOUT 10 ) +if(LG_CLIENT_PLATFORM_LINUX) + add_executable(config-platform-tests + config_platform_test.c + ../platform/Linux/config.c + ) + target_include_directories(config-platform-tests PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/../src" + ) + target_link_libraries(config-platform-tests + ${EXE_FLAGS} + lg_common + ) + add_test(NAME config-platform-tests + COMMAND config-platform-tests + ) + set_tests_properties(config-platform-tests PROPERTIES + TIMEOUT 10 + ) +endif() + if(LG_CLIENT_PLATFORM_WINDOWS) add_executable(common-windows-tests common_windows_test.c @@ -822,6 +845,7 @@ add_executable(config-message-tests ../src/message.c ../src/core.c ../src/config.c + ../platform/${LG_CLIENT_PLATFORM}/config.c ) target_compile_definitions(config-message-tests PRIVATE CIMGUI_DEFINE_ENUMS_AND_STRUCTS=1 diff --git a/client/tests/common_platform_test.c b/client/tests/common_platform_test.c index bbce7b04..0a885157 100644 --- a/client/tests/common_platform_test.c +++ b/client/tests/common_platform_test.c @@ -21,13 +21,17 @@ #include "test.h" #include "common/event.h" +#include "common/filesystem.h" #include "common/random.h" #include "common/semaphore.h" +#include "common/stringutils.h" #include "common/thread.h" #include "common/time.h" #include #include +#include +#include #include #define TEST_WAIT_NS UINT64_C(1000000000) @@ -161,6 +165,42 @@ static void testRandom(void) CHECK(memcmp(first, second, sizeof(first)) != 0); } +static void testFilesystem(void) +{ + char * directory; + char * file; + CHECK(alloc_sprintf(&directory, "%s/filesystem-test", LG_TEST_TMP_DIR) > 0); + CHECK(alloc_sprintf(&file, "%s/entry.txt", directory) > 0); + + lgDeleteFile(file); + lgDeleteDirectory(directory); + CHECK(lgPathType(directory) == LG_FILE_TYPE_MISSING); + CHECK(lgCreateDirectory(directory)); + CHECK(lgCreateDirectory(directory)); + CHECK(lgPathType(directory) == LG_FILE_TYPE_DIRECTORY); + + FILE * stream = lgOpenFile(file, "wb"); + CHECK(stream); + CHECK(fputs("Looking Glass", stream) >= 0); + CHECK(fclose(stream) == 0); + CHECK(lgPathType(file) == LG_FILE_TYPE_REGULAR); + + StringList entries = lgListDirectory(directory); + bool found = false; + CHECK(entries); + for (unsigned int i = 0; i < stringlist_count(entries); ++i) + if (!strcmp(stringlist_at(entries, i), "entry.txt")) + found = true; + CHECK(found); + stringlist_free(&entries); + + CHECK(lgDeleteFile(file)); + CHECK(lgDeleteDirectory(directory)); + CHECK(lgPathType(directory) == LG_FILE_TYPE_MISSING); + free(file); + free(directory); +} + int main(void) { lgTimeInit(); @@ -169,5 +209,6 @@ int main(void) testTimer(); testSemaphore(); testRandom(); + testFilesystem(); return 0; } diff --git a/client/tests/config_platform_test.c b/client/tests/config_platform_test.c new file mode 100644 index 00000000..fc66fb6f --- /dev/null +++ b/client/tests/config_platform_test.c @@ -0,0 +1,92 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "config_platform.h" +#include "test.h" + +#include +#include +#include +#include + +#define MAX_PATHS 3U + +struct PathList +{ + char * paths[MAX_PATHS]; + unsigned int count; +}; + +const char * lgConfigDir(void) +{ + return "/test/config"; +} + +static bool collectPath(const char * path, void * opaque) +{ + struct PathList * paths = opaque; + CHECK(paths->count < MAX_PATHS); + paths->paths[paths->count] = strdup(path); + CHECK(paths->paths[paths->count]); + ++paths->count; + return true; +} + +static bool stopAtFirstPath(const char * path, void * opaque) +{ + (void)path; + unsigned int * count = opaque; + ++*count; + return false; +} + +static void testCandidates(void) +{ + struct PathList paths = { 0 }; + CHECK(config_platformVisitDefaultFiles(collectPath, &paths)); + CHECK(paths.count == MAX_PATHS); + CHECK(strcmp(paths.paths[0], "/etc/looking-glass-client.ini") == 0); + + const struct passwd * password = getpwuid(getuid()); + CHECK(password); + + const size_t homeLength = strlen(password->pw_dir); + const char suffix[] = "/.looking-glass-client.ini"; + char * expected = malloc(homeLength + sizeof(suffix)); + CHECK(expected); + memcpy(expected, password->pw_dir, homeLength); + memcpy(expected + homeLength, suffix, sizeof(suffix)); + CHECK(strcmp(paths.paths[1], expected) == 0); + CHECK(strcmp(paths.paths[2], "/test/config/client.ini") == 0); + + free(expected); + for (unsigned int i = 0; i < paths.count; ++i) + free(paths.paths[i]); + + unsigned int count = 0; + CHECK(!config_platformVisitDefaultFiles(stopAtFirstPath, &count)); + CHECK(count == 1); +} + +int main(void) +{ + testCandidates(); + return 0; +} diff --git a/common/include/common/filesystem.h b/common/include/common/filesystem.h new file mode 100644 index 00000000..37e33513 --- /dev/null +++ b/common/include/common/filesystem.h @@ -0,0 +1,50 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifndef _H_LG_COMMON_FILESYSTEM_ +#define _H_LG_COMMON_FILESYSTEM_ + +#include "common/stringlist.h" + +#include +#include + +typedef enum LGFileType +{ + LG_FILE_TYPE_ERROR, + LG_FILE_TYPE_MISSING, + LG_FILE_TYPE_REGULAR, + LG_FILE_TYPE_DIRECTORY, + LG_FILE_TYPE_OTHER, +} +LGFileType; + +LGFileType lgPathType (const char * path); +LGFileType lgPathTypeNoFollow(const char * path); +bool lgCreateDirectory (const char * path); +bool lgDeleteFile (const char * path); +bool lgDeleteDirectory (const char * path); +FILE * lgOpenFile (const char * path, const char * mode); + +/* Returns an owned list of entry names, or NULL when the directory cannot be + * opened or read. The special entries "." and ".." are omitted. */ +StringList lgListDirectory (const char * path); + +#endif diff --git a/common/src/option.c b/common/src/option.c index 0e68ff0b..185b8272 100644 --- a/common/src/option.c +++ b/common/src/option.c @@ -20,6 +20,7 @@ #include "common/option.h" #include "common/debug.h" +#include "common/filesystem.h" #include "common/stringutils.h" #include @@ -506,7 +507,7 @@ static bool process_option_line(const char * module, const char * name, bool option_load(const char * filename) { - FILE * fp = fopen(filename, "r"); + FILE * fp = lgOpenFile(filename, "r"); if (!fp) return false; diff --git a/common/src/platform/linux/CMakeLists.txt b/common/src/platform/linux/CMakeLists.txt index 8785cdaa..1f0c0a0b 100644 --- a/common/src/platform/linux/CMakeLists.txt +++ b/common/src/platform/linux/CMakeLists.txt @@ -18,6 +18,7 @@ add_library(lg_common_platform_code STATIC time.c paths.c open.c + filesystem.c cpuinfo.c proctitle.c ) diff --git a/common/src/platform/linux/filesystem.c b/common/src/platform/linux/filesystem.c new file mode 100644 index 00000000..095e64ab --- /dev/null +++ b/common/src/platform/linux/filesystem.c @@ -0,0 +1,131 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "common/filesystem.h" + +#include "common/debug.h" +#include "common/stringutils.h" + +#include +#include +#include +#include +#include +#include + +static LGFileType pathType(const char * path, bool follow) +{ + struct stat value; + if ((follow ? stat(path, &value) : lstat(path, &value)) < 0) + return errno == ENOENT || errno == ENOTDIR ? + LG_FILE_TYPE_MISSING : LG_FILE_TYPE_ERROR; + + if (S_ISREG(value.st_mode)) + return LG_FILE_TYPE_REGULAR; + if (S_ISDIR(value.st_mode)) + return LG_FILE_TYPE_DIRECTORY; + return LG_FILE_TYPE_OTHER; +} + +LGFileType lgPathType(const char * path) +{ + return pathType(path, true); +} + +LGFileType lgPathTypeNoFollow(const char * path) +{ + return pathType(path, false); +} + +bool lgCreateDirectory(const char * path) +{ + if (mkdir(path, S_IRWXU) == 0) + return true; + + return errno == EEXIST && lgPathType(path) == LG_FILE_TYPE_DIRECTORY; +} + +bool lgDeleteFile(const char * path) +{ + return unlink(path) == 0; +} + +bool lgDeleteDirectory(const char * path) +{ + return rmdir(path) == 0; +} + +FILE * lgOpenFile(const char * path, const char * mode) +{ + return fopen(path, mode); +} + +StringList lgListDirectory(const char * path) +{ + DIR * directory = opendir(path); + if (!directory) + return NULL; + + StringList result = stringlist_new(true); + if (!result) + { + closedir(directory); + return NULL; + } + + int error = 0; + for (;;) + { + errno = 0; + struct dirent * entry = readdir(directory); + if (!entry) + { + error = errno; + break; + } + + if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) + continue; + + char * name = lg_strdup(entry->d_name); + if (!name) + goto fail; + + const unsigned int count = stringlist_count(result); + stringlist_push(result, name); + if (stringlist_count(result) != count + 1) + { + free(name); + goto fail; + } + } + + closedir(directory); + if (error) + goto fail_closed; + return result; + +fail: + closedir(directory); + +fail_closed: + stringlist_free(&result); + return NULL; +} diff --git a/common/src/platform/windows/CMakeLists.txt b/common/src/platform/windows/CMakeLists.txt index 561e264e..36c2afce 100644 --- a/common/src/platform/windows/CMakeLists.txt +++ b/common/src/platform/windows/CMakeLists.txt @@ -43,6 +43,7 @@ add_library(lg_common_platform_code STATIC win32.c paths.c open.c + filesystem.c cpuinfo.c display.c proctitle.c diff --git a/common/src/platform/windows/filesystem.c b/common/src/platform/windows/filesystem.c new file mode 100644 index 00000000..6ef30e77 --- /dev/null +++ b/common/src/platform/windows/filesystem.c @@ -0,0 +1,229 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "common/filesystem.h" + +#include "common/win32.h" + +#include +#include +#include +#include + +static wchar_t * widePath(const char * path) +{ + return lgUtf8ToWide(path); +} + +static LGFileType attributesType(DWORD attributes) +{ + if (attributes & FILE_ATTRIBUTE_DIRECTORY) + return LG_FILE_TYPE_DIRECTORY; + if (!(attributes & FILE_ATTRIBUTE_DEVICE)) + return LG_FILE_TYPE_REGULAR; + return LG_FILE_TYPE_OTHER; +} + +static LGFileType pathError(DWORD error) +{ + return error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND ? + LG_FILE_TYPE_MISSING : LG_FILE_TYPE_ERROR; +} + +static LGFileType pathType(const char * path, bool follow) +{ + wchar_t * wide = widePath(path); + if (!wide) + return LG_FILE_TYPE_ERROR; + + if (follow) + { + HANDLE file = CreateFileW(wide, 0, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); + const DWORD openError = file == INVALID_HANDLE_VALUE ? + GetLastError() : ERROR_SUCCESS; + free(wide); + if (file == INVALID_HANDLE_VALUE) + return pathError(openError); + + BY_HANDLE_FILE_INFORMATION info; + const bool result = GetFileInformationByHandle(file, &info); + const DWORD error = result ? ERROR_SUCCESS : GetLastError(); + CloseHandle(file); + if (!result) + return pathError(error); + return attributesType(info.dwFileAttributes); + } + + const DWORD attributes = GetFileAttributesW(wide); + const DWORD error = attributes == INVALID_FILE_ATTRIBUTES ? + GetLastError() : ERROR_SUCCESS; + free(wide); + if (attributes == INVALID_FILE_ATTRIBUTES) + return pathError(error); + if (attributes & FILE_ATTRIBUTE_REPARSE_POINT) + return LG_FILE_TYPE_OTHER; + return attributesType(attributes); +} + +LGFileType lgPathType(const char * path) +{ + return pathType(path, true); +} + +LGFileType lgPathTypeNoFollow(const char * path) +{ + return pathType(path, false); +} + +bool lgCreateDirectory(const char * path) +{ + wchar_t * wide = widePath(path); + if (!wide) + return false; + + bool result = CreateDirectoryW(wide, NULL); + if (!result && GetLastError() == ERROR_ALREADY_EXISTS) + { + const DWORD attributes = GetFileAttributesW(wide); + result = attributes != INVALID_FILE_ATTRIBUTES && + (attributes & FILE_ATTRIBUTE_DIRECTORY); + } + free(wide); + return result; +} + +bool lgDeleteFile(const char * path) +{ + wchar_t * wide = widePath(path); + if (!wide) + return false; + + const bool result = DeleteFileW(wide); + free(wide); + return result; +} + +bool lgDeleteDirectory(const char * path) +{ + wchar_t * wide = widePath(path); + if (!wide) + return false; + + const bool result = RemoveDirectoryW(wide); + free(wide); + return result; +} + +FILE * lgOpenFile(const char * path, const char * mode) +{ + wchar_t * widePathValue = lgUtf8ToWide(path); + wchar_t * wideMode = lgUtf8ToWide(mode); + if (!widePathValue || !wideMode) + { + free(widePathValue); + free(wideMode); + return NULL; + } + + FILE * result = _wfopen(widePathValue, wideMode); + free(widePathValue); + free(wideMode); + return result; +} + +StringList lgListDirectory(const char * path) +{ + wchar_t * wide = widePath(path); + if (!wide) + return NULL; + + const size_t length = wcslen(wide); + if (length > SIZE_MAX / sizeof(*wide) - 3) + { + free(wide); + return NULL; + } + + wchar_t * pattern = realloc(wide, (length + 3) * sizeof(*pattern)); + if (!pattern) + { + free(wide); + return NULL; + } + size_t offset = length; + if (length && pattern[length - 1] != L'\\' && + pattern[length - 1] != L'/') + pattern[offset++] = L'\\'; + pattern[offset++] = L'*'; + pattern[offset] = L'\0'; + + StringList result = stringlist_new(true); + if (!result) + { + free(pattern); + return NULL; + } + + WIN32_FIND_DATAW entry; + HANDLE find = FindFirstFileW(pattern, &entry); + free(pattern); + if (find == INVALID_HANDLE_VALUE) + { + const DWORD error = GetLastError(); + if (error == ERROR_FILE_NOT_FOUND) + return result; + stringlist_free(&result); + return NULL; + } + + do + { + if (!wcscmp(entry.cFileName, L".") || !wcscmp(entry.cFileName, L"..")) + continue; + + char * name = lgWideToUtf8(entry.cFileName); + if (!name) + goto fail; + + const unsigned int count = stringlist_count(result); + stringlist_push(result, name); + if (stringlist_count(result) != count + 1) + { + free(name); + goto fail; + } + } + while (FindNextFileW(find, &entry)); + + const DWORD error = GetLastError(); + FindClose(find); + if (error != ERROR_NO_MORE_FILES) + goto fail_closed; + return result; + +fail: + FindClose(find); + +fail_closed: + stringlist_free(&result); + return NULL; +}