From 9305d89a2e39dd88aa09222580d024cb9f5739c8 Mon Sep 17 00:00:00 2001 From: bubio Date: Mon, 15 Jun 2026 21:32:31 +0900 Subject: [PATCH 1/3] Supports symbolic links and macOS aliases (excluding Android) --- Builder/Windows/XM8.vcxproj | 8 +- Builder/Windows/XM8.vcxproj.filters | 18 ++-- CMakeLists.txt | 34 +++++++ Source/UI/diskmgr.cpp | 92 ++++++++++++------ Source/UI/diskmgr.h | 8 +- Source/UI/menu.cpp | 6 +- Source/UI/pathresolver.cpp | 89 +++++++++++++++++ Source/UI/pathresolver.h | 17 ++++ Source/UI/pathresolver_mac.mm | 84 ++++++++++++++++ Source/UI/platform.cpp | 145 +++++++++++++++------------- Source/UI/platform.h | 4 +- Source/UI/tapemgr.cpp | 71 ++++++++------ Source/UI/tapemgr.h | 2 + Tests/pathresolver_mac_test.mm | 93 ++++++++++++++++++ Tests/pathresolver_test.cpp | 84 ++++++++++++++++ 15 files changed, 611 insertions(+), 144 deletions(-) create mode 100644 Source/UI/pathresolver.cpp create mode 100644 Source/UI/pathresolver.h create mode 100644 Source/UI/pathresolver_mac.mm create mode 100644 Tests/pathresolver_mac_test.mm create mode 100644 Tests/pathresolver_test.cpp diff --git a/Builder/Windows/XM8.vcxproj b/Builder/Windows/XM8.vcxproj index c557ddc..b0de7be 100644 --- a/Builder/Windows/XM8.vcxproj +++ b/Builder/Windows/XM8.vcxproj @@ -388,8 +388,9 @@ - - + + + @@ -441,7 +442,8 @@ - + + diff --git a/Builder/Windows/XM8.vcxproj.filters b/Builder/Windows/XM8.vcxproj.filters index e5b95af..18ed7fb 100644 --- a/Builder/Windows/XM8.vcxproj.filters +++ b/Builder/Windows/XM8.vcxproj.filters @@ -156,9 +156,12 @@ ソース ファイル\UI - - ソース ファイル\UI - + + ソース ファイル\UI + + + ソース ファイル\UI + ソース ファイル\UI @@ -284,9 +287,12 @@ ヘッダー ファイル\UI - - ヘッダー ファイル\UI - + + ヘッダー ファイル\UI + + + ヘッダー ファイル\UI + ヘッダー ファイル\UI diff --git a/CMakeLists.txt b/CMakeLists.txt index 65bcc01..5db5e31 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,6 +37,10 @@ set(PROJECT_VERSION 1.7.8) # Project Name project(XM8 VERSION ${PROJECT_VERSION}) +if(APPLE) + enable_language(OBJC OBJCXX) +endif() + include(CTest) # Packaging options @@ -116,6 +120,7 @@ set(SRCS Source/UI/menu.cpp Source/UI/menuitem.cpp Source/UI/menulist.cpp + Source/UI/pathresolver.cpp Source/UI/platform.cpp Source/UI/setting.cpp Source/UI/softkey.cpp @@ -143,6 +148,10 @@ set(SRCS Source/ePC-8801MA/vm/pc8801/pc8801.cpp ) +if(APPLE) + list(APPEND SRCS Source/UI/pathresolver_mac.mm) +endif() + set(BIN_TARGET "xm8") add_executable(${BIN_TARGET} MACOSX_BUNDLE ${SRCS} Builder/macOS/AppIcon.icns) target_compile_definitions(${BIN_TARGET} PRIVATE SDL _PC8801MA) @@ -186,6 +195,9 @@ option(ENABLE_PACKAGING "Enable CPack packaging" ON) # Link SDL2 and xBRZ to xm8 target using interface target from Dependencies.cmake target_link_libraries(${BIN_TARGET} PRIVATE XM8::SDL xbrz) +if(APPLE) + target_link_libraries(${BIN_TARGET} PRIVATE "-framework Foundation") +endif() if(BUILD_TESTING) add_executable(clidisk_test @@ -207,6 +219,28 @@ if(BUILD_TESTING) target_compile_definitions(d88probe_test PRIVATE SDL _PC8801MA) target_link_libraries(d88probe_test PRIVATE XM8::SDL) add_test(NAME d88probe_test COMMAND d88probe_test) + + add_executable(pathresolver_test + Tests/pathresolver_test.cpp + Source/UI/pathresolver.cpp + ) + if(APPLE) + target_sources(pathresolver_test PRIVATE Source/UI/pathresolver_mac.mm) + target_link_libraries(pathresolver_test PRIVATE "-framework Foundation") + endif() + target_include_directories(pathresolver_test PRIVATE Source/UI) + add_test(NAME pathresolver_test COMMAND pathresolver_test) + + if(APPLE) + add_executable(pathresolver_mac_test + Tests/pathresolver_mac_test.mm + Source/UI/pathresolver.cpp + Source/UI/pathresolver_mac.mm + ) + target_include_directories(pathresolver_mac_test PRIVATE Source/UI) + target_link_libraries(pathresolver_mac_test PRIVATE "-framework Foundation") + add_test(NAME pathresolver_mac_test COMMAND pathresolver_mac_test) + endif() endif() # Packaging diff --git a/Source/UI/diskmgr.cpp b/Source/UI/diskmgr.cpp index 015dc7c..a7f3dcf 100644 --- a/Source/UI/diskmgr.cpp +++ b/Source/UI/diskmgr.cpp @@ -17,6 +17,7 @@ #include "upd765a.h" #include "vm.h" #include "d88probe.h" +#include "pathresolver.h" #include "diskmgr.h" // @@ -42,7 +43,8 @@ DiskManager::DiskManager() name_list = NULL; wp_list = NULL; signal = 0; - path[0] = '\0'; + path[0] = '\0'; + resolved_path[0] = '\0'; dir[0] = '\0'; state_path[0] = '\0'; next_bank = 0; @@ -105,23 +107,28 @@ void DiskManager::SetVM(VM *v) // bool DiskManager::Probe(const char *filename, int *banks) { - return ProbeD88Image(filename, banks); + char resolved[_MAX_PATH * 3]; + return ResolvePathForIO(filename, resolved, sizeof(resolved)) && + ProbeD88Image(resolved, banks); } // // Open() // open disk // -bool DiskManager::Open(const char *filename, int bank) -{ - char *ptr; - char *last; - - // save path - if (strlen(filename) >= sizeof(path)) { - return false; - } - strcpy(path, filename); +bool DiskManager::Open(const char *filename, int bank) +{ + char *ptr; + char *last; + char candidate[_MAX_PATH * 3]; + + // save path + if (filename == NULL || strlen(filename) >= sizeof(path) || + ResolvePathForIO(filename, candidate, sizeof(candidate)) == false) { + return false; + } + strcpy(path, filename); + strcpy(resolved_path, candidate); // save directory strcpy(dir, path); @@ -147,7 +154,11 @@ bool DiskManager::Open(const char *filename, int bank) // reopen disk // bool DiskManager::Open(int bank) -{ +{ + if (ResolvePath() == false) { + return false; + } + // close Close(); @@ -164,7 +175,7 @@ bool DiskManager::Open(int bank) current_bank = bank; // open - vm->open_disk(drive, (_TCHAR*)path, current_bank); + vm->open_disk(drive, (_TCHAR*)resolved_path, current_bank); // ready ready = true; @@ -379,12 +390,15 @@ int DiskManager::GetBanks() // SetBank // change disk bank // -bool DiskManager::SetBank(int bank) -{ - if ((bank >= 0) && (bank < num_of_banks)) { - // open (dummy to access file) - vm->close_disk(drive); - vm->open_disk(drive, (_TCHAR*)path, bank); +bool DiskManager::SetBank(int bank) +{ + if ((bank >= 0) && (bank < num_of_banks)) { + if (ResolvePath() == false) { + return false; + } + // open (dummy to access file) + vm->close_disk(drive); + vm->open_disk(drive, (_TCHAR*)resolved_path, bank); // close vm->close_disk(drive); @@ -406,12 +420,17 @@ void DiskManager::ProcessMgr() { if (next_timer > 0) { next_timer--; - if (next_timer == 0) { - current_bank = next_bank; - vm->open_disk(drive, (_TCHAR*)path, current_bank); - } - } -} + if (next_timer == 0) { + current_bank = next_bank; + if (ResolvePath() == true) { + vm->open_disk(drive, (_TCHAR*)resolved_path, current_bank); + } + else { + Close(); + } + } + } +} // // Load() @@ -449,15 +468,24 @@ void DiskManager::Load(FILEIO *fio) // void DiskManager::Save(FILEIO *fio) { - fio->Fwrite(path, 1, sizeof(path)); + fio->Fwrite(path, 1, sizeof(path)); fio->FputBool(ready); fio->FputInt32(current_bank); fio->FputInt32(next_bank); fio->FputInt32(next_timer); } -// -// Analyze() +// +// ResolvePath() +// resolve logical disk path for I/O +// +bool DiskManager::ResolvePath() +{ + return ResolvePathForIO(path, resolved_path, sizeof(resolved_path)); +} + +// +// Analyze() // analyze d88 header // bool DiskManager::Analyze() @@ -470,14 +498,14 @@ bool DiskManager::Analyze() int bank; char *ptr; - if (ProbeD88Image(path, &num_of_banks, &len) == false) { + if (ProbeD88Image(resolved_path, &num_of_banks, &len) == false) { return false; } - if (fio.Fopen(path, FILEIO_READ_BINARY) == false) { + if (fio.Fopen(resolved_path, FILEIO_READ_BINARY) == false) { return false; } - readonly = fio.IsProtected(path); + readonly = fio.IsProtected(resolved_path); // malloc name_list = (char*)SDL_malloc(len); diff --git a/Source/UI/diskmgr.h b/Source/UI/diskmgr.h index b3181da..ffdffae 100644 --- a/Source/UI/diskmgr.h +++ b/Source/UI/diskmgr.h @@ -81,6 +81,8 @@ class DiskManager // save state private: + bool ResolvePath(); + // resolve path used for I/O bool Analyze(); // analyze d88 header int drive; @@ -89,8 +91,10 @@ class DiskManager // virtual machine UPD765A *upd765a; // fdc - char path[_MAX_PATH * 3]; - // disk path + char path[_MAX_PATH * 3]; + // disk path + char resolved_path[_MAX_PATH * 3]; + // disk path used for I/O char dir[_MAX_PATH * 3]; // disk dir char state_path[_MAX_PATH * 3]; diff --git a/Source/UI/menu.cpp b/Source/UI/menu.cpp index 4ba7efe..51e7e4b 100644 --- a/Source/UI/menu.cpp +++ b/Source/UI/menu.cpp @@ -3083,7 +3083,7 @@ void Menu::CmdFile(int id) } #endif // __ANDROID__ - if (platform->MakePath(file_target, name) == true) { + if (platform->MakePath(file_target, name, true) == true) { MakeExpect(name); strcpy(file_dir, file_target); EnterFile(); @@ -3092,7 +3092,9 @@ void Menu::CmdFile(int id) } // normal file - platform->MakePath(file_target, name); + if (platform->MakePath(file_target, name, false) == false) { + return; + } // tape ? if ((file_id == MENU_CMT_PLAY) || (file_id == MENU_CMT_REC)) { diff --git a/Source/UI/pathresolver.cpp b/Source/UI/pathresolver.cpp new file mode 100644 index 0000000..c11b59d --- /dev/null +++ b/Source/UI/pathresolver.cpp @@ -0,0 +1,89 @@ +#include +#include + +#if defined(__linux__) || defined(__APPLE__) || defined(__ANDROID__) +#include +#include +#endif + +#include "pathresolver.h" + +#ifdef __APPLE__ +bool ResolveMacAlias(const char *path, char *resolved, size_t capacity); +#endif + +bool ResolvePathForIO(const char *path, char *resolved, size_t capacity) +{ + char candidate[4096]; + + if (path == nullptr || resolved == nullptr || capacity == 0) { + return false; + } + +#ifdef __APPLE__ + if (!ResolveMacAlias(path, candidate, sizeof(candidate))) { + return false; + } +#else + const size_t length = std::strlen(path); + if (length >= sizeof(candidate)) { + return false; + } + std::memcpy(candidate, path, length + 1); +#endif + +#if defined(__linux__) || defined(__APPLE__) || defined(__ANDROID__) + char canonical[4096]; + if (realpath(candidate, canonical) != nullptr) { + const size_t length = std::strlen(canonical); + if (length >= capacity) { + return false; + } + std::memcpy(resolved, canonical, length + 1); + return true; + } + + struct stat file_stat; + if (lstat(candidate, &file_stat) == 0 || errno != ENOENT) { + return false; + } +#endif + + const size_t length = std::strlen(candidate); + if (length >= capacity) { + return false; + } + std::memcpy(resolved, candidate, length + 1); + return true; +} + +PathKind InspectPath(const char *path, char *resolved, size_t capacity) +{ + char local_path[4096]; + char *target = resolved; + size_t target_capacity = capacity; + + if (target == nullptr) { + target = local_path; + target_capacity = sizeof(local_path); + } + if (!ResolvePathForIO(path, target, target_capacity)) { + return PATH_KIND_UNAVAILABLE; + } + +#if defined(__linux__) || defined(__APPLE__) || defined(__ANDROID__) + struct stat file_stat; + if (stat(target, &file_stat) != 0) { + return PATH_KIND_UNAVAILABLE; + } + if (S_ISREG(file_stat.st_mode)) { + return PATH_KIND_FILE; + } + if (S_ISDIR(file_stat.st_mode)) { + return PATH_KIND_DIRECTORY; + } + return PATH_KIND_OTHER; +#else + return PATH_KIND_OTHER; +#endif +} diff --git a/Source/UI/pathresolver.h b/Source/UI/pathresolver.h new file mode 100644 index 0000000..c5c3029 --- /dev/null +++ b/Source/UI/pathresolver.h @@ -0,0 +1,17 @@ +#ifndef PATHRESOLVER_H +#define PATHRESOLVER_H + +#include + +enum PathKind { + PATH_KIND_UNAVAILABLE = 0, + PATH_KIND_FILE, + PATH_KIND_DIRECTORY, + PATH_KIND_OTHER +}; + +bool ResolvePathForIO(const char *path, char *resolved, size_t capacity); +PathKind InspectPath(const char *path, char *resolved = nullptr, + size_t capacity = 0); + +#endif diff --git a/Source/UI/pathresolver_mac.mm b/Source/UI/pathresolver_mac.mm new file mode 100644 index 0000000..3b30f73 --- /dev/null +++ b/Source/UI/pathresolver_mac.mm @@ -0,0 +1,84 @@ +#import + +#include +#include +#include + +#include "pathresolver.h" + +namespace { + +bool CopyPath(const char *source, char *destination, size_t capacity) +{ + const size_t length = std::strlen(source); + if (length >= capacity) { + return false; + } + std::memcpy(destination, source, length + 1); + return true; +} + +} + +bool ResolveMacAlias(const char *path, char *resolved, size_t capacity) +{ + if (path == nullptr || resolved == nullptr || capacity == 0) { + return false; + } + + @autoreleasepool { + NSString *string = [NSString stringWithUTF8String:path]; + if (string == nil) { + return false; + } + + NSURL *url = [NSURL fileURLWithPath:string]; + NSNumber *is_alias = nil; + NSError *error = nil; + if (![url getResourceValue:&is_alias + forKey:NSURLIsAliasFileKey + error:&error]) { + struct stat file_stat; + if (lstat(path, &file_stat) != 0 && errno == ENOENT) { + // Nonexistent paths must remain usable for file creation. + return CopyPath(path, resolved, capacity); + } + return false; + } + if (![is_alias boolValue]) { + return CopyPath(path, resolved, capacity); + } + + NSMutableSet *visited = [NSMutableSet set]; + for (int depth = 0; depth < 16; depth++) { + NSString *current_path = [url path]; + if (current_path == nil || [visited containsObject:current_path]) { + return false; + } + [visited addObject:current_path]; + + url = [NSURL URLByResolvingAliasFileAtURL:url + options:(NSURLBookmarkResolutionWithoutUI | + NSURLBookmarkResolutionWithoutMounting) + error:&error]; + if (url == nil || ![url isFileURL]) { + return false; + } + + is_alias = nil; + error = nil; + if (![url getResourceValue:&is_alias + forKey:NSURLIsAliasFileKey + error:&error]) { + return false; + } + if (![is_alias boolValue]) { + const char *file_path = [url fileSystemRepresentation]; + return file_path != nullptr && + CopyPath(file_path, resolved, capacity); + } + } + } + + return false; +} diff --git a/Source/UI/platform.cpp b/Source/UI/platform.cpp index 804db15..6564ac6 100644 --- a/Source/UI/platform.cpp +++ b/Source/UI/platform.cpp @@ -20,6 +20,7 @@ #include "classes.h" #include "app.h" #include "converter.h" +#include "pathresolver.h" #include "platform.h" #ifdef __ANDROID__ @@ -54,6 +55,19 @@ #define LOCALE_UTF8 "UTF-8" // UTF-8 +static bool IsSupportedFile(char *path) +{ + char d88[] = ".d88"; + char cmt[] = ".cmt"; + char t88[] = ".t88"; + char n80[] = ".n80"; + + return check_file_extension(path, d88) || + check_file_extension(path, cmt) || + check_file_extension(path, t88) || + check_file_extension(path, n80); +} + // // Platform() // constructor @@ -79,6 +93,7 @@ Platform::Platform(App *a) dir_handle = NULL; dir_name[0] = '\0'; dir_name_utf8[0] = '\0'; + find_dir[0] = '\0'; dir_up = false; #endif // __linux__ || __APPLE__ } @@ -298,6 +313,11 @@ const char* Platform::FindFirst(const char *dir, Uint32 *info) #if defined(__linux__) || defined(__APPLE__) DIR *dir_ret; + if (strlen(dir) >= sizeof(find_dir)) { + return NULL; + } + strcpy(find_dir, dir); + // Find .. dir_up = FindUp(dir); @@ -418,10 +438,9 @@ const char* Platform::FindNext(Uint32 *info) #if defined(__linux__) || defined(__APPLE__) struct dirent *entry; - Converter *converter; - - // get converter - converter = app->GetConverter(); + char entry_path[_MAX_PATH * 3]; + char resolved_path[_MAX_PATH * 3]; + PathKind kind; // find for (;;) { @@ -432,36 +451,41 @@ const char* Platform::FindNext(Uint32 *info) return NULL; } - // check file extension - if (entry->d_type == DT_REG) { - // normal file - if (check_file_extension(entry->d_name, _T(".d88")) == false && - check_file_extension(entry->d_name, _T(".cmt")) == false && - check_file_extension(entry->d_name, _T(".t88")) == false && - check_file_extension(entry->d_name, _T(".n80")) == false) { - // unsupported file - continue; - } - } - // check . - if (entry->d_name[0] != '.') { + if (entry->d_name[0] == '.') { + continue; + } + + if (strlen(find_dir) + strlen(entry->d_name) >= sizeof(entry_path)) { + continue; + } + strcpy(entry_path, find_dir); + strcat(entry_path, entry->d_name); + kind = InspectPath(entry_path, resolved_path, sizeof(resolved_path)); + if (kind == PATH_KIND_DIRECTORY) { break; } + if (kind != PATH_KIND_FILE) { + continue; + } + if (IsSupportedFile(resolved_path) == false) { + continue; + } + break; } // name strcpy(dir_name, entry->d_name); // directory ? - if (entry->d_type == DT_DIR) { + if (kind == PATH_KIND_DIRECTORY) { if (dir_name[strlen(dir_name) - 1] != '/') { strcat(dir_name, "/"); } } // type - *info = (Uint32)entry->d_type; + *info = kind == PATH_KIND_DIRECTORY ? DT_DIR : DT_REG; return dir_name; #endif // __liunx__ || __APPLE__ @@ -474,39 +498,20 @@ const char* Platform::FindNext(Uint32 *info) // bool Platform::FindUp(const char *dir) { - struct dirent *entry; - DIR *dir_ret; + char parent[_MAX_PATH * 3]; + struct stat file_stat; // root ? if (strcmp(dir, "/") == 0) { return false; } - // open directory - dir_ret = opendir(dir); - if (dir_ret == NULL) { + if (strlen(dir) + 2 >= sizeof(parent)) { return false; } - - // find loop - for (;;) { - entry = readdir(dir_ret); - if (entry == NULL) { - break; - } - - // check '..' - if (strcmp(entry->d_name, "..") == 0) { - if (entry->d_type == DT_DIR) { - closedir(dir_ret); - return true; - } - } - } - - // close and false - closedir(dir_ret); - return false; + strcpy(parent, dir); + strcat(parent, ".."); + return stat(parent, &file_stat) == 0 && S_ISDIR(file_stat.st_mode); } #endif // __liunx__ || __APPLE__ @@ -545,7 +550,7 @@ bool Platform::IsDir(Uint32 info) // MakePath() // make path name from dir(UTF-8) and name(SHIFT-JIS) // -bool Platform::MakePath(char *dir, const char *name) +bool Platform::MakePath(char *dir, const char *name, bool directory) { #if defined(_WIN32) && defined(UNICODE) wchar_t wide_dir[MAX_PATH]; @@ -585,6 +590,8 @@ bool Platform::MakePath(char *dir, const char *name) wcscat_s(wide_dir, SDL_arraysize(wide_dir), wide_name); } + (void)directory; + // GetFullPathName GetFullPathName(wide_dir, SDL_arraysize(wide_name), wide_name, &part); @@ -602,34 +609,40 @@ bool Platform::MakePath(char *dir, const char *name) #endif // _WIN32 && UNICODE #if defined(__linux__) || defined(__APPLE__) - Converter *converter; struct stat filestat; + char joined[_MAX_PATH * 3]; + char resolved[_MAX_PATH * 3]; + size_t dir_length; + size_t name_length; + + dir_length = strlen(dir); + name_length = strlen(name); + while (name_length > 0 && + (name[name_length - 1] == '/' || name[name_length - 1] == '\\')) { + name_length--; + } + if (dir_length + name_length >= sizeof(joined)) { + return false; + } + memcpy(joined, dir, dir_length); + memcpy(joined + dir_length, name, name_length); + joined[dir_length + name_length] = '\0'; - // get converter - converter = app->GetConverter(); - - // SHIFT-JIS to UTF-8 - // converter->SjisToUtf(name, dir_name); - strcpy(dir_name, name); - - // cat - strcat(dir, dir_name); + if (directory == false) { + strcpy(dir, joined); + return true; + } - // realpath - if (realpath(dir, dir_name) == NULL) { + if (!ResolvePathForIO(joined, resolved, sizeof(resolved)) || + realpath(resolved, dir_name) == NULL || + stat(dir_name, &filestat) != 0 || + !S_ISDIR(filestat.st_mode)) { return false; } - - // directory ? - if (stat(dir_name, &filestat) == 0) { - if (S_ISDIR(filestat.st_mode)) { - if (dir_name[strlen(dir_name) - 1] != '/') { - strcat(dir_name, "/"); - } - } + if (dir_name[strlen(dir_name) - 1] != '/') { + strcat(dir_name, "/"); } - // realpath to dir strcpy(dir, dir_name); return true; diff --git a/Source/UI/platform.h b/Source/UI/platform.h index 12e416c..419db95 100644 --- a/Source/UI/platform.h +++ b/Source/UI/platform.h @@ -35,7 +35,7 @@ class Platform // find next file bool IsDir(Uint32 info); // check directory - bool MakePath(char *dir, const char *name); + bool MakePath(char *dir, const char *name, bool directory); // make path from dir(UTF-8) and name(SHIFT-JIS) // file date and time @@ -75,6 +75,8 @@ class Platform // file name (shift-jis) char dir_name_utf8[_MAX_PATH * 3]; // file name (utf8-mac) + char find_dir[_MAX_PATH * 3]; + // directory currently being enumerated bool dir_up; // FindUp() result #endif // __linux__ || __APPLE__ diff --git a/Source/UI/tapemgr.cpp b/Source/UI/tapemgr.cpp index c62802e..1aa4036 100644 --- a/Source/UI/tapemgr.cpp +++ b/Source/UI/tapemgr.cpp @@ -14,6 +14,7 @@ #include "common.h" #include "vm.h" #include "app.h" +#include "pathresolver.h" #include "tapemgr.h" // @@ -31,6 +32,7 @@ TapeManager::TapeManager() // path and dir path[0] = '\0'; + resolved_path[0] = '\0'; dir[0] = '\0'; state_path[0] = '\0'; nullstr[0] = '\0'; @@ -85,16 +87,16 @@ void TapeManager::SetVM(VM *v) // bool TapeManager::Play(const char *filename) { - // Eject - Eject(); - // open if (Open(filename, false) == false) { return false; } + // Eject + Eject(); + // virtual machine - vm->play_tape((_TCHAR*)path); + vm->play_tape((_TCHAR*)resolved_path); // mount ok mount_play = true; @@ -108,16 +110,16 @@ bool TapeManager::Play(const char *filename) // bool TapeManager::Rec(const char *filename) { - // Eject - Eject(); - // open if (Open(filename, true) == false) { return false; } + // Eject + Eject(); + // virtual machine - vm->rec_tape((_TCHAR*)path); + vm->rec_tape((_TCHAR*)resolved_path); // mount ok mount_rec = true; @@ -133,44 +135,49 @@ bool TapeManager::Open(const char *filename, bool rec) { char *ptr; char *last; + char candidate_path[_MAX_PATH * 3]; + char candidate_dir[_MAX_PATH * 3]; + char candidate_resolved[_MAX_PATH * 3]; FILEIO fileio; bool ret; - // specify filename ? - if (filename != NULL) { - // save path - if (strlen(filename) >= sizeof(path)) { - return false; - } - strcpy(path, filename); - - // save directory - strcpy(dir, path); - ptr = dir; - last = dir; - - // search last '\\' or '/' - while (*ptr != '\0') { - if ((*ptr == '\\') || (*ptr == '/')) { - last = ptr; - } - ptr++; + const char *source = filename != NULL ? filename : path; + if (source[0] == '\0' || strlen(source) >= sizeof(candidate_path)) { + return false; + } + strcpy(candidate_path, source); + strcpy(candidate_dir, candidate_path); + ptr = candidate_dir; + last = candidate_dir; + + // search last '\\' or '/' + while (*ptr != '\0') { + if ((*ptr == '\\') || (*ptr == '/')) { + last = ptr; } + ptr++; + } + last[1] = '\0'; - // end mark - last[1] = '\0'; + if (ResolvePathForIO(candidate_path, candidate_resolved, + sizeof(candidate_resolved)) == false) { + return false; } // open test if (rec == true) { - ret = fileio.Fopen(path, FILEIO_READ_WRITE_NEW_BINARY); + ret = fileio.Fopen(candidate_resolved, + FILEIO_READ_WRITE_NEW_BINARY); } else { - ret = fileio.Fopen(path, FILEIO_READ_BINARY); + ret = fileio.Fopen(candidate_resolved, FILEIO_READ_BINARY); } if (ret == true) { // close immediately fileio.Fclose(); + strcpy(path, candidate_path); + strcpy(dir, candidate_dir); + strcpy(resolved_path, candidate_resolved); } return ret; @@ -270,7 +277,7 @@ void TapeManager::Load(FILEIO *fio) // void TapeManager::Save(FILEIO *fio) { - fio->Fwrite(state_path, 1, sizeof(state_path)); + fio->Fwrite(path, 1, sizeof(path)); fio->FputBool(mount_play); fio->FputBool(mount_rec); } diff --git a/Source/UI/tapemgr.h b/Source/UI/tapemgr.h index 1930fd7..c6bdedf 100644 --- a/Source/UI/tapemgr.h +++ b/Source/UI/tapemgr.h @@ -61,6 +61,8 @@ class TapeManager // mount flag (rec) char path[_MAX_PATH * 3]; // tape path + char resolved_path[_MAX_PATH * 3]; + // tape path used for I/O char dir[_MAX_PATH * 3]; // tape dir char state_path[_MAX_PATH * 3]; diff --git a/Tests/pathresolver_mac_test.mm b/Tests/pathresolver_mac_test.mm new file mode 100644 index 0000000..1e7ddc1 --- /dev/null +++ b/Tests/pathresolver_mac_test.mm @@ -0,0 +1,93 @@ +#import + +#include +#include +#include + +#include "pathresolver.h" + +namespace { + +int failures = 0; + +void Check(bool condition, const char *message) +{ + if (!condition) { + std::cerr << "FAIL: " << message << '\n'; + failures++; + } +} + +bool CreateAlias(NSURL *target, NSURL *alias) +{ + NSError *error = nil; + NSData *bookmark = [target bookmarkDataWithOptions: + NSURLBookmarkCreationSuitableForBookmarkFile + includingResourceValuesForKeys:nil + relativeToURL:nil + error:&error]; + return bookmark != nil && + [NSURL writeBookmarkData:bookmark toURL:alias options:0 error:&error]; +} + +bool MatchesCanonicalPath(const char *actual, NSURL *expected) +{ + char canonical[1024]; + return realpath([[expected path] fileSystemRepresentation], canonical) != + nullptr && std::strcmp(actual, canonical) == 0; +} + +} + +int main() +{ + @autoreleasepool { + NSFileManager *manager = [NSFileManager defaultManager]; + NSURL *root = [NSURL fileURLWithPath: + @"/tmp/xm8-pathresolver-mac-test" isDirectory:YES]; + [manager removeItemAtURL:root error:nil]; + Check([manager createDirectoryAtURL:root + withIntermediateDirectories:YES attributes:nil error:nil], + "create temporary directory"); + + NSURL *target = [root URLByAppendingPathComponent:@"target.d88"]; + NSURL *moved = [root URLByAppendingPathComponent:@"moved.d88"]; + NSURL *alias = [root URLByAppendingPathComponent:@"disk alias"]; + NSURL *directory = [root URLByAppendingPathComponent:@"directory" + isDirectory:YES]; + NSURL *directory_alias = [root URLByAppendingPathComponent: + @"directory alias"]; + + Check([manager createFileAtPath:[target path] contents:[NSData data] + attributes:nil], "create alias target"); + Check([manager createDirectoryAtURL:directory + withIntermediateDirectories:YES attributes:nil error:nil], + "create directory alias target"); + Check(CreateAlias(target, alias), "create file alias"); + Check(CreateAlias(directory, directory_alias), "create directory alias"); + + char resolved[1024]; + Check(InspectPath([[alias path] fileSystemRepresentation], resolved, + sizeof(resolved)) == PATH_KIND_FILE, "classify file alias"); + Check(MatchesCanonicalPath(resolved, target), "resolve file alias"); + Check(InspectPath([[directory_alias path] fileSystemRepresentation], + resolved, sizeof(resolved)) == PATH_KIND_DIRECTORY, + "classify directory alias"); + + Check([manager moveItemAtURL:target toURL:moved error:nil], + "move alias target"); + Check(ResolvePathForIO([[alias path] fileSystemRepresentation], + resolved, sizeof(resolved)), "resolve moved alias"); + Check(MatchesCanonicalPath(resolved, moved), + "follow moved alias target"); + Check([manager removeItemAtURL:moved error:nil], + "remove alias target"); + Check(InspectPath([[alias path] fileSystemRepresentation], resolved, + sizeof(resolved)) == PATH_KIND_UNAVAILABLE, + "reject broken alias"); + + [manager removeItemAtURL:root error:nil]; + } + + return failures == 0 ? 0 : 1; +} diff --git a/Tests/pathresolver_test.cpp b/Tests/pathresolver_test.cpp new file mode 100644 index 0000000..581abc7 --- /dev/null +++ b/Tests/pathresolver_test.cpp @@ -0,0 +1,84 @@ +#include +#include +#include +#include + +#if defined(__linux__) || defined(__APPLE__) +#include +#include +#endif + +#include "pathresolver.h" + +namespace { + +int failures = 0; + +void Check(bool condition, const char *message) +{ + if (!condition) { + std::cerr << "FAIL: " << message << '\n'; + failures++; + } +} + +} + +int main() +{ +#if defined(__linux__) || defined(__APPLE__) + char temporary[] = "/tmp/xm8-pathresolver-XXXXXX"; + char *directory = mkdtemp(temporary); + Check(directory != nullptr, "create temporary directory"); + if (directory == nullptr) { + return 1; + } + + char file_path[1024]; + char file_link[1024]; + char dir_link[1024]; + char broken_link[1024]; + std::snprintf(file_path, sizeof(file_path), "%s/disk.d88", directory); + std::snprintf(file_link, sizeof(file_link), "%s/file-link", directory); + std::snprintf(dir_link, sizeof(dir_link), "%s/dir-link", directory); + std::snprintf(broken_link, sizeof(broken_link), "%s/broken-link", directory); + + FILE *file = std::fopen(file_path, "wb"); + Check(file != nullptr, "create regular file"); + if (file != nullptr) { + std::fclose(file); + } + Check(symlink(file_path, file_link) == 0, "create file symlink"); + Check(symlink(directory, dir_link) == 0, "create directory symlink"); + Check(symlink("/path/that/does/not/exist", broken_link) == 0, + "create broken symlink"); + + char resolved[1024]; + Check(InspectPath(file_path, resolved, sizeof(resolved)) == PATH_KIND_FILE, + "classify regular file"); + Check(InspectPath(file_link, resolved, sizeof(resolved)) == PATH_KIND_FILE, + "classify file symlink"); + char canonical[1024]; + Check(realpath(file_path, canonical) != nullptr && + std::strcmp(resolved, canonical) == 0, "resolve file symlink"); + Check(InspectPath(dir_link, resolved, sizeof(resolved)) == + PATH_KIND_DIRECTORY, "classify directory symlink"); + Check(realpath(directory, canonical) != nullptr && + std::strcmp(resolved, canonical) == 0, "resolve directory symlink"); + Check(InspectPath(broken_link, resolved, sizeof(resolved)) == + PATH_KIND_UNAVAILABLE, "reject broken symlink"); + + unlink(broken_link); + unlink(dir_link); + unlink(file_link); + unlink(file_path); + rmdir(directory); +#else + char resolved[32]; + Check(ResolvePathForIO("disk.d88", resolved, sizeof(resolved)), + "copy normal path"); + Check(std::strcmp(resolved, "disk.d88") == 0, "preserve normal path"); +#endif + + return failures == 0 ? 0 : 1; +} From cc38cce0f0463592a7b665d3568c7cbc676fc16d Mon Sep 17 00:00:00 2001 From: bubio Date: Mon, 15 Jun 2026 22:24:22 +0900 Subject: [PATCH 2/3] Fixed an issue causing build errors on platforms other than macOS --- Source/UI/pathresolver.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Source/UI/pathresolver.cpp b/Source/UI/pathresolver.cpp index c11b59d..85f75ba 100644 --- a/Source/UI/pathresolver.cpp +++ b/Source/UI/pathresolver.cpp @@ -25,11 +25,11 @@ bool ResolvePathForIO(const char *path, char *resolved, size_t capacity) return false; } #else - const size_t length = std::strlen(path); - if (length >= sizeof(candidate)) { + const size_t input_length = std::strlen(path); + if (input_length >= sizeof(candidate)) { return false; } - std::memcpy(candidate, path, length + 1); + std::memcpy(candidate, path, input_length + 1); #endif #if defined(__linux__) || defined(__APPLE__) || defined(__ANDROID__) @@ -49,11 +49,11 @@ bool ResolvePathForIO(const char *path, char *resolved, size_t capacity) } #endif - const size_t length = std::strlen(candidate); - if (length >= capacity) { + const size_t output_length = std::strlen(candidate); + if (output_length >= capacity) { return false; } - std::memcpy(resolved, candidate, length + 1); + std::memcpy(resolved, candidate, output_length + 1); return true; } From f62b821f65e9a685b02399a6f833b288b8708908 Mon Sep 17 00:00:00 2001 From: bubio Date: Mon, 15 Jun 2026 22:42:07 +0900 Subject: [PATCH 3/3] Update RaspberryPiOS_armhf.yml --- .github/workflows/RaspberryPiOS_armhf.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/RaspberryPiOS_armhf.yml b/.github/workflows/RaspberryPiOS_armhf.yml index 11b3289..04600d3 100644 --- a/.github/workflows/RaspberryPiOS_armhf.yml +++ b/.github/workflows/RaspberryPiOS_armhf.yml @@ -39,6 +39,7 @@ jobs: deb [arch=armhf] http://ports.ubuntu.com/ ${FLAVOR}-updates universe deb [arch=armhf] http://ports.ubuntu.com/ ${FLAVOR} multiverse deb [arch=armhf] http://ports.ubuntu.com/ ${FLAVOR}-updates multiverse + deb [arch=armhf] http://ports.ubuntu.com/ ${FLAVOR}-security main restricted universe multiverse deb [arch=armhf] http://ports.ubuntu.com/ ${FLAVOR}-backports main restricted universe multiverse LIST sudo sed -E -i 's/deb (http|file|mirror)/deb [arch=amd64,i386] \1/' /etc/apt/sources.list