From 949895c87fb0c5494b6e3fdd7e32d3467bec826f Mon Sep 17 00:00:00 2001 From: Jon Stephens Date: Wed, 16 Feb 2022 10:02:05 -0700 Subject: [PATCH 1/5] bail instead of UB if we get too many file filters --- osdialog_win.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osdialog_win.c b/osdialog_win.c index ecc00f9..42bf1f5 100644 --- a/osdialog_win.c +++ b/osdialog_win.c @@ -292,14 +292,17 @@ char* osdialog_file(osdialog_file_action action, const char* dir, const char* fi for (; filters; filters = filters->next) { fLen += snprintf(fBuf + fLen, sizeof(fBuf) - fLen, "%s", filters->name); + if (fLen + 1 >= sizeof(fBuf)) return NULL; fBuf[fLen++] = '\0'; for (osdialog_filter_patterns* patterns = filters->patterns; patterns; patterns = patterns->next) { fLen += snprintf(fBuf + fLen, sizeof(fBuf) - fLen, "*.%s", patterns->pattern); if (patterns->next) fLen += snprintf(fBuf + fLen, sizeof(fBuf) - fLen, ";"); } + if (fLen + 1 >= sizeof(fBuf)) return NULL; fBuf[fLen++] = '\0'; } + if (fLen + 1 >= sizeof(fBuf)) return NULL; fBuf[fLen++] = '\0'; // Don't use utf8_to_wchar() because this is not a NULL-terminated string. From 6b30650826c3bc9b52e566dc01eb6190addd8123 Mon Sep 17 00:00:00 2001 From: Jon Stephens Date: Fri, 25 Feb 2022 14:16:02 -0700 Subject: [PATCH 2/5] add support for multiple file open on Windows --- osdialog.h | 6 ++++ osdialog_win.c | 84 +++++++++++++++++++++++++++++++++++++++++++++++--- test.c | 17 ++++++++++ 3 files changed, 102 insertions(+), 5 deletions(-) diff --git a/osdialog.h b/osdialog.h index febae2b..ead1ceb 100644 --- a/osdialog.h +++ b/osdialog.h @@ -55,6 +55,12 @@ typedef enum { OSDIALOG_OPEN, OSDIALOG_OPEN_DIR, OSDIALOG_SAVE, + /** + Select multiple files. + Results returned as a null-seperated list of null-terminated strings, ending with a final '\0'. + Windows only. + */ + OSDIALOG_OPEN_MULTIPLE, } osdialog_file_action; /** Linked list of patterns. */ diff --git a/osdialog_win.c b/osdialog_win.c index 42bf1f5..a6c1b6b 100644 --- a/osdialog_win.c +++ b/osdialog_win.c @@ -5,6 +5,9 @@ #include #include "osdialog.h" +#define snprintf(buf, len, format,...) _snprintf_s(buf, len, len, format, __VA_ARGS__) +#define snwprintf(buf, len, format,...) _snwprintf_s(buf, len, len, format, __VA_ARGS__) + static char* wchar_to_utf8(const wchar_t* s) { if (!s) @@ -265,14 +268,15 @@ char* osdialog_file(osdialog_file_action action, const char* dir, const char* fi ofn.Flags = OFN_EXPLORER | OFN_OVERWRITEPROMPT | OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR; // filename - wchar_t strFile[MAX_PATH] = L""; + #define MAX_RESULT_SIZE (1024 * 10) + wchar_t strFile[MAX_RESULT_SIZE] = L""; if (filename) { wchar_t* filenameW = utf8_to_wchar(filename); - snwprintf(strFile, MAX_PATH, L"%S", filenameW); + snwprintf(strFile, MAX_RESULT_SIZE, L"%lS", filenameW); OSDIALOG_FREE(filenameW); } ofn.lpstrFile = strFile; - ofn.nMaxFile = MAX_PATH; + ofn.nMaxFile = MAX_RESULT_SIZE; // dir wchar_t strInitialDir[MAX_PATH] = L""; @@ -313,7 +317,11 @@ char* osdialog_file(osdialog_file_action action, const char* dir, const char* fi } BOOL success; - if (action == OSDIALOG_OPEN) { + if (action == OSDIALOG_OPEN_MULTIPLE) { + ofn.Flags |= OFN_ALLOWMULTISELECT; + success = GetOpenFileNameW(&ofn); + } + else if (action == OSDIALOG_OPEN) { success = GetOpenFileNameW(&ofn); } else { @@ -327,7 +335,73 @@ char* osdialog_file(osdialog_file_action action, const char* dir, const char* fi if (!success) { return NULL; } - return wchar_to_utf8(strFile); + + if (action == OSDIALOG_OPEN_MULTIPLE) { + // GetOpenFileNameW returns [dirname, file, file, ...] as a null-seperated list of null-terminated strings + + if (strFile[0] == 0 && strFile[1] == 0) return NULL; + + const wchar_t* dirname = strFile; + int dirnameUTF8Len = WideCharToMultiByte(CP_UTF8, 0, dirname, -1, NULL, 0, NULL, NULL) - 1; + + // get stats + int filesUTF8Len = 0; + int numFiles = 0; + const wchar_t* lastItemStart = strFile; + const wchar_t* firstFile = NULL; + for (int i = 0; i < MAX_RESULT_SIZE - 1; i++) { + if (strFile[i] == 0) { + ++numFiles; + if (!firstFile && lastItemStart != dirname) firstFile = lastItemStart; + filesUTF8Len += WideCharToMultiByte(CP_UTF8, 0, lastItemStart, -1, NULL, 0, NULL, NULL) - 1; + + lastItemStart = &strFile[i + 1]; + if (strFile[i + 1] == 0) break; // end of list + } + } + + if (numFiles == 1) { + // ...but if there's only one file GetOpenFileNameW just returns a single item with the dirname added + return wchar_to_utf8(strFile); + } + + // right now numFiles/filesUTF8Len include dirname (first entry), correct + filesUTF8Len -= dirnameUTF8Len; + --numFiles; + + int resultUTF8Len = (dirnameUTF8Len + 2) * numFiles + filesUTF8Len + 1;// dirname + '/' + file + '\0' + char* ret = OSDIALOG_MALLOC(resultUTF8Len); + memset(ret, 0, resultUTF8Len); + + // convert chars and build result + lastItemStart = firstFile; + char* retPos = ret; + for (int i = firstFile - strFile; i < MAX_RESULT_SIZE - 1; i++) { + if (strFile[i] == 0) {// i is at the end of a filename + // write dir + WideCharToMultiByte(CP_UTF8, 0, dirname, -1, retPos, dirnameUTF8Len, NULL, NULL); + retPos += dirnameUTF8Len; + + *retPos = '\\'; ++retPos; + + // write file + int fileUTF8Len = WideCharToMultiByte(CP_UTF8, 0, lastItemStart, -1, NULL, 0, NULL, NULL) - 1; + WideCharToMultiByte(CP_UTF8, 0, lastItemStart, -1, retPos, fileUTF8Len, NULL, NULL); + retPos += fileUTF8Len; + + *retPos = '\0'; ++retPos; + + lastItemStart = &strFile[i + 1]; + + if (strFile[i + 1] == 0) break; // end of list, memset earlier already wrote a final '\0' + } + } + + return ret; + } + else { + return wchar_to_utf8(strFile); + } } } diff --git a/test.c b/test.c index 3e62ed8..13e1a4b 100644 --- a/test.c +++ b/test.c @@ -157,5 +157,22 @@ int main(int argc, char* argv[]) { fprintf(stderr, "\t#%02x%02x%02x%02x\n", color.r, color.g, color.b, color.a); } + // Open multiple files + if (test < 0 || test == 10) { + fprintf(stderr, "multi file open in cwd\n"); + char* filename = osdialog_file(OSDIALOG_OPEN_MULTIPLE, ".", "こんにちは", NULL); + if (filename) { + char* file = filename; + while (strlen(file)) { + fprintf(stderr, "\t%s\n", file); + file += strlen(file) + 1; + } + free(filename); + } + else { + fprintf(stderr, "\tCanceled\n"); + } + } + return 0; } From bd94a608db105216c505291243b2ca4d51975a64 Mon Sep 17 00:00:00 2001 From: Jon Stephens Date: Fri, 25 Feb 2022 14:50:33 -0700 Subject: [PATCH 3/5] add support for multiple file open on GTK2/3 and address a warning when building test.c --- osdialog.h | 3 ++- osdialog_gtk2.c | 46 +++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/osdialog.h b/osdialog.h index ead1ceb..f5de68c 100644 --- a/osdialog.h +++ b/osdialog.h @@ -7,6 +7,7 @@ extern "C" { #include #include +#include #ifndef OSDIALOG_MALLOC @@ -58,7 +59,7 @@ typedef enum { /** Select multiple files. Results returned as a null-seperated list of null-terminated strings, ending with a final '\0'. - Windows only. + Windows/GTK2/GKT3 only. */ OSDIALOG_OPEN_MULTIPLE, } osdialog_file_action; diff --git a/osdialog_gtk2.c b/osdialog_gtk2.c index 629a1c7..6d8edf3 100644 --- a/osdialog_gtk2.c +++ b/osdialog_gtk2.c @@ -85,6 +85,11 @@ char* osdialog_file(osdialog_file_action action, const char* dir, const char* fi acceptText = "Open"; gtkAction = GTK_FILE_CHOOSER_ACTION_OPEN; } + else if (action == OSDIALOG_OPEN_MULTIPLE) { + title = "Open Files"; + acceptText = "Open"; + gtkAction = GTK_FILE_CHOOSER_ACTION_OPEN; + } else if (action == OSDIALOG_OPEN_DIR) { title = "Open Folder"; acceptText = "Open Folder"; @@ -115,6 +120,9 @@ char* osdialog_file(osdialog_file_action action, const char* dir, const char* fi gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), fileFilter); } + if (action == OSDIALOG_OPEN_MULTIPLE) + gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog), TRUE); + if (action == OSDIALOG_SAVE) gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dialog), TRUE); @@ -124,16 +132,44 @@ char* osdialog_file(osdialog_file_action action, const char* dir, const char* fi if (action == OSDIALOG_SAVE && filename) gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog), filename); - char* chosen_filename = NULL; + GSList* chosenFiles = NULL; if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) { - chosen_filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog)); + chosenFiles = gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(dialog)); } gtk_widget_destroy(dialog); char* result = NULL; - if (chosen_filename) { - result = osdialog_strndup(chosen_filename, strlen(chosen_filename)); - g_free(chosen_filename); + if (chosenFiles) { + if (action != OSDIALOG_OPEN_MULTIPLE) { + result = osdialog_strndup(chosenFiles->data, strlen(chosenFiles->data)); + g_free(chosenFiles->data); + } + else { + // calculate result length + int resultLen = 2;// include space for full terminator in case of an empty list + GSList* item = chosenFiles; + while (item) { + resultLen += strlen(item->data) + 1; + item = item->next; + } + + result = OSDIALOG_MALLOC(resultLen); + memset(result, 0, resultLen); + char* resultPos = result; + + item = chosenFiles; + while (item) { + int len = strlen(item->data); + memcpy(resultPos, item->data, len + 1); + resultPos += len + 1; + + g_free(item->data); + item = item->next; + } + // final terminator was nulled via memset earlier + } + + g_slist_free(chosenFiles); } while (gtk_events_pending()) From 079f16ff5f543cfff87e5aa9d556f2a8f9a2673d Mon Sep 17 00:00:00 2001 From: Jon Stephens Date: Fri, 25 Feb 2022 15:55:46 -0700 Subject: [PATCH 4/5] add support for multiple file open on Mac --- osdialog.h | 2 +- osdialog_mac.m | 36 +++++++++++++++++++++++++++++++----- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/osdialog.h b/osdialog.h index f5de68c..d40017a 100644 --- a/osdialog.h +++ b/osdialog.h @@ -59,7 +59,7 @@ typedef enum { /** Select multiple files. Results returned as a null-seperated list of null-terminated strings, ending with a final '\0'. - Windows/GTK2/GKT3 only. + Windows/GTK2/GKT3/Mac only. */ OSDIALOG_OPEN_MULTIPLE, } osdialog_file_action; diff --git a/osdialog_mac.m b/osdialog_mac.m index 02f820b..7f647e7 100644 --- a/osdialog_mac.m +++ b/osdialog_mac.m @@ -112,7 +112,7 @@ int osdialog_message(osdialog_message_level level, osdialog_message_buttons butt // NSOpenPanel is a subclass of NSSavePanel. Not defined for OSDIALOG_SAVE. NSOpenPanel* open_panel; - if (action == OSDIALOG_OPEN || action == OSDIALOG_OPEN_DIR) { + if (action == OSDIALOG_OPEN || action == OSDIALOG_OPEN_DIR || action == OSDIALOG_OPEN_MULTIPLE) { panel = open_panel = [NSOpenPanel openPanel]; } else { @@ -140,6 +140,10 @@ int osdialog_message(osdialog_message_level level, osdialog_message_buttons butt if (action == OSDIALOG_OPEN || action == OSDIALOG_OPEN_DIR) { [open_panel setAllowsMultipleSelection:NO]; } + else if (action == OSDIALOG_OPEN_MULTIPLE) { + [open_panel setAllowsMultipleSelection:YES]; + } + if (action == OSDIALOG_OPEN) { [open_panel setCanChooseDirectories:NO]; [open_panel setCanChooseFiles:YES]; @@ -170,10 +174,32 @@ int osdialog_message(osdialog_message_level level, osdialog_message_buttons butt #define OK NSOKButton #endif if (response == OK) { - NSURL* result_url = [panel URL]; - NSString* result_str = [result_url path]; - // Don't use NSString.length because it returns the number of the UTF-16 code units, not the number of bytes. - result = osdialog_strdup([result_str UTF8String]); + if (action != OSDIALOG_OPEN_MULTIPLE) { + NSURL* result_url = [panel URL]; + NSString* result_str = [result_url path]; + // Don't use NSString.length because it returns the number of the UTF-16 code units, not the number of bytes. + result = osdialog_strdup([result_str UTF8String]); + } + else { + // note: can't use NSMutableString and appendString; it doens't handle the null bytes right + NSArray* results = [panel URLs]; + + int resultLen = 2; + for (NSURL *fileURL in results) { + resultLen += strlen([[fileURL path] UTF8String]) + 1; + } + + result = OSDIALOG_MALLOC(resultLen); + memset(result, 0, resultLen); + char* resultPos = result; + + for (NSURL *fileURL in results) { + const char* pathUTF8 = [[fileURL path] UTF8String]; + int len = strlen(pathUTF8); + memcpy(resultPos, pathUTF8, len + 1); + resultPos += len + 1; + } + } } [keyWindow makeKeyAndOrderFront:nil]; From 007e4ba0cfcf53523bc9032805a033a6c4e71668 Mon Sep 17 00:00:00 2001 From: Jon Stephens Date: Fri, 25 Feb 2022 16:31:57 -0700 Subject: [PATCH 5/5] add support for multiple file open on Zenity --- osdialog.h | 1 - osdialog_zenity.c | 38 ++++++++++++++++++++++++++++++++------ 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/osdialog.h b/osdialog.h index d40017a..d44729b 100644 --- a/osdialog.h +++ b/osdialog.h @@ -59,7 +59,6 @@ typedef enum { /** Select multiple files. Results returned as a null-seperated list of null-terminated strings, ending with a final '\0'. - Windows/GTK2/GKT3/Mac only. */ OSDIALOG_OPEN_MULTIPLE, } osdialog_file_action; diff --git a/osdialog_zenity.c b/osdialog_zenity.c index e1d8ee5..c37ed00 100644 --- a/osdialog_zenity.c +++ b/osdialog_zenity.c @@ -174,6 +174,13 @@ char* osdialog_file(osdialog_file_action action, const char* dir, const char* fi if (action == OSDIALOG_OPEN) { // This is the default } + else if (action == OSDIALOG_OPEN_MULTIPLE) { + args[argIndex++] = osdialog_strdup("--multiple"); + // separate file names with \x1E "record seperator" because the default "|" can casually appear in + // filenames and using \x00 for a seperator isn't an option + #define FILE_SEP_CHAR '\x1E' + args[argIndex++] = osdialog_strdup("--separator=\x1E"); + } else if (action == OSDIALOG_OPEN_DIR) { args[argIndex++] = osdialog_strdup("--directory"); } @@ -205,17 +212,36 @@ char* osdialog_file(osdialog_file_action action, const char* dir, const char* fi } args[argIndex++] = NULL; - char outBuf[4096 + 1]; + char outBuf[1024 * 10]; int ret = string_list_exec(zenityBin, (const char* const*) args, outBuf, sizeof(outBuf), NULL, 0); string_list_clear(args); if (ret != 0) return NULL; - // Remove trailing newline - size_t outLen = strlen(outBuf); - if (outLen > 0) - outBuf[outLen - 1] = '\0'; - return osdialog_strdup(outBuf); + if (action != OSDIALOG_OPEN_MULTIPLE) { + // Remove trailing newline + size_t outLen = strlen(outBuf); + if (outLen > 0) + outBuf[outLen - 1] = '\0'; + return osdialog_strdup(outBuf); + } + else { + if (!strlen(outBuf)) return NULL; + + // nix the ending newline, and make sure there's a \x00\x00 at the end + size_t resultLen = strlen(outBuf) + (2 - 1); + char* result = OSDIALOG_MALLOC(resultLen); + memcpy(result, outBuf, resultLen - 2); + result[resultLen - 1] = 0; + result[resultLen - 2] = 0; + + // Convert \x1E to \x00, + for (size_t i = 0; i < resultLen; i++) { + if (result[i] == FILE_SEP_CHAR) result[i] = 0; + } + + return result; + } }