commit 27505a573a4fb41b681de41d3b80dde0e3fac625
parent 767655d6eb5e7d11a499ca54f7eda36f19423a09
Author: Bharatvaj Hemanth <bharatvaj@yahoo.com>
Date: Tue, 13 Oct 2026 22:57:34 +0530
parent 767655d6eb5e7d11a499ca54f7eda36f19423a09
Author: Bharatvaj Hemanth <bharatvaj@yahoo.com>
Date: Tue, 13 Oct 2026 22:57:34 +0530
Preserve long piped lines and handle allocation failures
5 files changed, 137 insertions(+), 31 deletions(-)
diff --git a/HACKING b/HACKING @@ -14,9 +14,11 @@ Dependencies ------------ External source is pulled with mor from requirements.ini: - bash C:/Users/bhara/Projects/mor/mor.sh . + bash /path/to/mor.sh . -Currently this fetches fzy into deps/fzy. The build uses only fzy's matcher: +This fetches fzy v1.1 into deps/fzy (requirements.ini pins the tag). +For an existing checkout, remove deps/fzy before rerunning mor: mor does not +update directories already present. The build uses only fzy's matcher: deps/fzy/src/match.c deps/fzy/src/match.h @@ -80,7 +82,7 @@ Release checklist 1. Run make, make test, and optionally make stress (about 155 MiB of synthetic rows; validates no-data ListBox navigation, full-file fiber search, and progress state). Run `powershell.exe -NoProfile -ExecutionPolicy Bypass -File - test_pipe.ps1` to verify rows appear before pipe EOF. Run + test_pipe.ps1` to verify complete long rows appear before pipe EOF. Run `powershell.exe -NoProfile -ExecutionPolicy Bypass -File test_typing.ps1` to measure typing latency with 20,000 streamed rows and stdin still open. 2. Check:
diff --git a/requirements.ini b/requirements.ini @@ -2,4 +2,4 @@ /=deps [.] -fzy=https://github.com/jhawthorn/fzy.git@master +fzy=https://github.com/jhawthorn/fzy.git@v1.1
diff --git a/test_pipe.ps1 b/test_pipe.ps1 @@ -30,15 +30,24 @@ try { } } if ($count -ne 1) { throw "Expected row before pipe closes; got $count, hwnd=$win, list=$list, exited=$($p.HasExited)" } + $p.StandardInput.WriteLine('long-' + ('x' * 8192)) for ($j = 0; $j -lt 3000; $j++) { $p.StandardInput.WriteLine("streamed-$j") } $p.StandardInput.Flush() $more = 0 - for ($i = 0; $i -lt 100 -and $more -lt 2049; $i++) { + for ($i = 0; $i -lt 100 -and $more -lt 3002; $i++) { Start-Sleep -Milliseconds 50 $more = [NativeMenu]::SendMessage($list, 0x18B, [IntPtr]::Zero, [IntPtr]::Zero).ToInt32() } - if ($more -lt 2049) { throw "Expected streamed list to grow while stdin open; got $more" } - Write-Output "List reached $more rows before EOF." + if ($more -ne 3002) { throw "Expected 3,002 rows including one long line before EOF; got $more" } + $edit = [NativeMenu]::FindWindowEx($win, [IntPtr]::Zero, 'Edit', $null) + if ($edit -eq [IntPtr]::Zero) { throw 'No edit control' } + [void][NativeMenu]::SendMessage($list, 0x186, [IntPtr]1, [IntPtr]::Zero) # LB_SETCURSEL + if ([NativeMenu]::SendMessage($list, 0x188, [IntPtr]::Zero, [IntPtr]::Zero).ToInt32() -ne 1) { throw 'Could not select the long line' } + $output = $p.StandardOutput.ReadToEndAsync() + [void][NativeMenu]::SendMessage($edit, 0x100, [IntPtr]13, [IntPtr]::Zero) # WM_KEYDOWN, Enter + if (-not $p.WaitForExit(5000)) { throw 'wmenu did not exit after Enter' } + if ($output.GetAwaiter().GetResult() -ne ('long-' + ('x' * 8192))) { throw 'Long row was not preserved on stdout' } + Write-Output "List showed $more complete rows before EOF; long row output intact." } finally { if (-not $p.HasExited) { $p.Kill(); $p.WaitForExit() } $p.Dispose()
diff --git a/test_stream.c b/test_stream.c @@ -17,6 +17,24 @@ static LRESULT CALLBACK TestWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) int main(int argc, char **argv) { + FILE *input = tmpfile(); + assert(input); + char long_row[8193]; + memset(long_row, 'x', sizeof long_row - 1); + long_row[sizeof long_row - 1] = 0; + assert(fputs(long_row, input) >= 0); + assert(fputs("\r\nnext\r\npartial", input) >= 0); + rewind(input); + char *line = NULL; + size_t capacity = 0; + assert(read_line(input, &line, &capacity) == 1); + assert(!strcmp(line, long_row)); + assert(read_line(input, &line, &capacity) == 1 && !strcmp(line, "next")); + assert(read_line(input, &line, &capacity) == 1 && !strcmp(line, "partial")); + assert(read_line(input, &line, &capacity) == 0); + free(line); + fclose(input); + InitializeCriticalSection(&items_lock); INITCOMMONCONTROLSEX icc = { sizeof icc, ICC_PROGRESS_CLASS }; assert(InitCommonControlsEx(&icc)); @@ -43,7 +61,14 @@ int main(int argc, char **argv) bool stress = argc > 1 && !strcmp(argv[1], "stress"); ShowWindow(main_hwnd, SW_SHOWNOACTIVATE); ShowWindow(list_hwnd, SW_SHOWNOACTIVATE); - add_item("item-0"); + assert(add_item("item-0")); + int saved_capacity = item_capacity; + char **saved_items = items; + item_count = item_capacity = INT_MAX; + assert(!add_item("overflow")); + assert(item_count == INT_MAX && items == saved_items); + item_count = 1; + item_capacity = saved_capacity; WndProc(main_hwnd, WM_STDIN_CHUNK, 0, 0); UpdateWindow(list_hwnd); assert(SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0) == 1); @@ -56,7 +81,7 @@ int main(int argc, char **argv) memset(buf + len, 'x', 2699 - len); buf[2699] = 0; /* About 155 MiB of line data in stress mode. */ } - add_item(buf); + assert(add_item(buf)); } refill_list(""); assert(SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0) == 60000); @@ -93,7 +118,7 @@ int main(int argc, char **argv) location_count = location_cursor = 0; list_move(-2); list_move(5); - add_item("extra-row"); + assert(add_item("extra-row")); input_done = false; WndProc(main_hwnd, WM_TIMER, TIMER_PROGRESS, 0); /* Partial batch before EOF. */ input_done = true; @@ -187,7 +212,7 @@ int main(int argc, char **argv) assert(!GetUpdateRect(list_hwnd, NULL, FALSE)); SendMessageW(list_hwnd, LB_SETCURSEL, 59999, 0); int kept_top = (int)SendMessageW(list_hwnd, LB_GETTOPINDEX, 0, 0); - add_item("item-60000"); + assert(add_item("item-60000")); refill_list(""); assert(SendMessageW(list_hwnd, LB_GETCOUNT, 0, 0) == 60002); assert(SendMessageW(list_hwnd, LB_GETCURSEL, 0, 0) == 59999); @@ -198,7 +223,7 @@ int main(int argc, char **argv) refill_list("tail-special"); search_step(); input_done = false; - add_item("tail-special"); + assert(add_item("tail-special")); while (search_pos < item_count) search_step(); assert(match_count == 1 && matches[0] == 60002); input_done = true;
diff --git a/wmenu.c b/wmenu.c @@ -7,6 +7,8 @@ #include <stdlib.h> #include <string.h> #include <io.h> +#include <limits.h> +#include <stdint.h> #include <wctype.h> #include "arg.h" @@ -80,15 +82,35 @@ static char *text_encoded(HWND hwnd) return s; } -static void add_item(const char *s) +static bool add_item(const char *s) { + char *copy = _strdup(s ? s : ""); + if (!copy) return false; EnterCriticalSection(&items_lock); if (item_count == item_capacity) { - item_capacity = item_capacity ? item_capacity * 2 : 1024; - items = (char **)realloc(items, sizeof(char *) * item_capacity); + if (item_capacity > INT_MAX / 2) { + LeaveCriticalSection(&items_lock); + free(copy); + return false; + } + int next = item_capacity ? item_capacity * 2 : 1024; + if ((size_t)next > SIZE_MAX / sizeof(*items)) { + LeaveCriticalSection(&items_lock); + free(copy); + return false; + } + char **grown = (char **)realloc(items, (size_t)next * sizeof(*items)); + if (!grown) { + LeaveCriticalSection(&items_lock); + free(copy); + return false; + } + items = grown; + item_capacity = next; } - items[item_count++] = _strdup(s ? s : ""); + items[item_count++] = copy; LeaveCriticalSection(&items_lock); + return true; } static void make_font(void) @@ -111,21 +133,49 @@ static void set_font(HWND hwnd) if (ui_font && hwnd) SendMessageW(hwnd, WM_SETFONT, (WPARAM)ui_font, TRUE); } +/* Read one complete line, growing the buffer rather than splitting long rows. */ +static int read_line(FILE *stream, char **line, size_t *capacity) +{ + size_t len = 0; + for (;;) { + if (*capacity - len < 2) { + if (*capacity > INT_MAX / 2) return -1; + size_t next = *capacity ? *capacity * 2 : 4096; + char *grown = (char *)realloc(*line, next); + if (!grown) return -1; + *line = grown; + *capacity = next; + } + if (!fgets(*line + len, (int)(*capacity - len), stream)) { + if (ferror(stream)) return -1; + if (!len) return 0; + break; + } + len += strlen(*line + len); + if (len && (*line)[len - 1] == '\n') break; + } + if (len && (*line)[len - 1] == '\n') --len; + if (len && (*line)[len - 1] == '\r') --len; + (*line)[len] = 0; + return 1; +} + static DWORD WINAPI read_stdin_thread(void *unused) { - char buf[4096]; - int n = 0; + char *line = NULL; + size_t capacity = 0; + int n = 0, status; (void)unused; - while (fgets(buf, sizeof buf, stdin)) { - buf[strcspn(buf, "\r\n")] = 0; - if (*buf) { - add_item(buf); + while ((status = read_line(stdin, &line, &capacity)) > 0) { + if (*line) { + if (!add_item(line)) { status = -1; break; } if ((++n == 1 || n % CHUNK_LINES == 0) && !InterlockedExchange(&refresh_pending, 1)) PostMessageW(main_hwnd, WM_STDIN_CHUNK, 0, 0); } } + free(line); loading_items = false; - PostMessageW(main_hwnd, WM_STDIN_DONE, 0, 0); + PostMessageW(main_hwnd, WM_STDIN_DONE, status < 0, 0); return 0; } @@ -293,7 +343,9 @@ static VOID WINAPI search_fiber_run(void *unused) for (; search_pos < end; ++search_pos) { if (!has_match(search_query, items[search_pos])) continue; if (match_count == match_capacity) { + if (match_capacity > INT_MAX / 2) { PostQuitMessage(1); search_pos = item_count; break; } int next = match_capacity ? match_capacity * 2 : 1024; + if ((size_t)next > SIZE_MAX / sizeof(*matches)) { PostQuitMessage(1); search_pos = item_count; break; } int *grown = (int *)realloc(matches, (size_t)next * sizeof(*matches)); if (!grown) { PostQuitMessage(1); search_pos = item_count; break; } matches = grown; @@ -387,8 +439,22 @@ static char *current_text(void) static void add_selection(void) { char *s = current_text(); - if (!s || !*s) { free(s); return; } - selected = (char **)realloc(selected, sizeof(char *) * (selected_count + 1)); + if (!s) { fputs("wmenu: out of memory selecting item\n", stderr); PostQuitMessage(1); return; } + if (!*s) { free(s); return; } + if (selected_count == INT_MAX || (size_t)(selected_count + 1) > SIZE_MAX / sizeof(*selected)) { + free(s); + fputs("wmenu: too many selections\n", stderr); + PostQuitMessage(1); + return; + } + char **grown = (char **)realloc(selected, sizeof(*selected) * (size_t)(selected_count + 1)); + if (!grown) { + free(s); + fputs("wmenu: out of memory selecting item\n", stderr); + PostQuitMessage(1); + return; + } + selected = grown; selected[selected_count++] = s; SetWindowTextW(input_hwnd, L""); if (has_items) refill_list(""); @@ -396,12 +462,11 @@ static void add_selection(void) static void finish(void) { + char *s = input_hwnd ? current_text() : NULL; + if (input_hwnd && !s) { fputs("wmenu: out of memory accepting item\n", stderr); PostQuitMessage(1); return; } for (int i = 0; i < selected_count; ++i) printf("%s\n", selected[i]); - if (input_hwnd) { - char *s = current_text(); - if (s && *s) fputs(s, stdout); - free(s); - } + if (s && *s) fputs(s, stdout); + free(s); PostQuitMessage(0); } @@ -582,6 +647,7 @@ static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) } return 0; case WM_STDIN_DONE: + if (wp) { fputs("wmenu: failed to read stdin (I/O or memory error)\n", stderr); PostQuitMessage(1); return 0; } input_done = true; has_items = item_count > 0; KillTimer(hwnd, TIMER_PROGRESS); @@ -705,6 +771,7 @@ int main(int argc, char **argv) } break; case 'p': { wchar_t *w = widen(EARGF(usage())); + if (!w) { fputs("wmenu: out of memory reading prompt\n", stderr); return 1; } has_prompt = true; wcsncpy(prompt, w, 299); prompt[299] = 0; @@ -715,6 +782,7 @@ int main(int argc, char **argv) if (argv[0][1] == 'n') { argv[0]++; w = widen(EARGF(usage())); + if (!w) { fputs("wmenu: out of memory reading font name\n", stderr); return 1; } custom_font = true; wcsncpy(font_name, w, LF_FACESIZE - 1); font_name[LF_FACESIZE - 1] = 0; @@ -803,7 +871,9 @@ int main(int argc, char **argv) SetFocus(input_hwnd); if (loading_items) { SetTimer(main_hwnd, TIMER_PROGRESS, 30, NULL); - CreateThread(NULL, 0, read_stdin_thread, NULL, 0, NULL); + HANDLE reader = CreateThread(NULL, 0, read_stdin_thread, NULL, 0, NULL); + if (!reader) { fputs("wmenu: cannot start stdin reader\n", stderr); return 1; } + CloseHandle(reader); } while (GetMessageW(&msg, NULL, 0, 0) > 0) { TranslateMessage(&msg);
: nply/wmenu