From 03a475aaee0a039262c7e8a324c995e3bb6766e8 Mon Sep 17 00:00:00 2001 From: Xavier Roche Date: Sun, 26 Jul 2026 09:47:37 +0200 Subject: [PATCH 1/2] Stop the first run hanging on the language list LANGINTKEY() returns "" and never NULL, so an index naming no language yielded an empty name, and that empty name is what ends the enumeration loop in Cabout::OnInitDialog. Making the guard live in 5345d2d turned the miss into a "???" placeholder instead, three characters long, so the loop never ends: it AddString()s into the combo box forever, one core pinned, no message pump. The About box is shown on the first run only, gated on Interface\FirstRun, and that flag is written to the registry BEFORE the modal opens. So a clean install hangs on launch, killing it leaves the flag set, and every later run skips the block and looks fine. That is why it survived: it reproduces once per machine. Hand back the empty name, which is what LANGINTKEY() already produces, and state the contract at the declaration so it does not get "fixed" a third time. The self-heal that 5345d2d was actually for, falling back to English on a stale index, is untouched. --selftest now walks the list under a sane bound and fails instead of hanging, and CI asserts the check ran. Nothing else in CI can see this: --selftest exits long before any dialog, so the hang was invisible to every green build. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_019HXNEMWtyrkW7xoD8ewcR3 Signed-off-by: Xavier Roche --- .github/workflows/windows-build.yml | 5 +++++ WinHTTrack/WinHTTrack.cpp | 23 +++++++++++++++++++++++ WinHTTrack/newlang.cpp | 9 ++++----- WinHTTrack/newlang.h | 3 +++ 4 files changed, 35 insertions(+), 5 deletions(-) diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml index 502937a..a3acb2b 100644 --- a/.github/workflows/windows-build.yml +++ b/.github/workflows/windows-build.yml @@ -646,6 +646,11 @@ jobs: if ("$so" -notmatch 'MBCS->UTF-8 ok') { throw "selftest did not exercise the MBCS->UTF-8 conversion" } + # The language list is walked until an empty name. Losing that terminator hung the + # first-run About box on every clean install, where nothing else here can reach. + if ("$so" -notmatch 'language list ends after \d+ entries') { + throw "selftest did not check that the language list terminates" + } # --selftest throws one exception on purpose. A crash report that resolves nothing # is indistinguishable from a working one until someone needs it, so assert the diff --git a/WinHTTrack/WinHTTrack.cpp b/WinHTTrack/WinHTTrack.cpp index be5fcfb..c204124 100755 --- a/WinHTTrack/WinHTTrack.cpp +++ b/WinHTTrack/WinHTTrack.cpp @@ -332,6 +332,29 @@ BOOL CWinHTTrackApp::InitInstance() printf("MBCS->UTF-8 ok\n"); } } + /* The language list is enumerated by walking the index until LANG_LOAD() reports an + empty name. When that terminator was lost the first-run About box spun forever, + and nothing else here could see it: --selftest exits long before that dialog. */ + { + const int LANG_SANE_MAX = 512; /* lang.def ships a few dozen */ + const int saved = QLANG_T(-1); + int i; + for(i=0 ; i= LANG_SANE_MAX) { + fprintf(stderr, "FATAL: the language list never ends: LANG_LOAD() lost its empty-name terminator\n"); + fflush(stderr); + ExitProcess(4); + } + printf("language list ends after %d entries\n", i); + } /* Exercise the crash reporter for real: a Release PDB built without line info, or a first-chance hook that never registered, both still produce a plausible-looking report that names nothing. Only throwing proves the chain resolves. */ diff --git a/WinHTTrack/newlang.cpp b/WinHTTrack/newlang.cpp index cacb850..b04b02f 100755 --- a/WinHTTrack/newlang.cpp +++ b/WinHTTrack/newlang.cpp @@ -325,12 +325,11 @@ void LANG_LOAD(char* limit_to) { hashname=LANGINTKEY(name); } - /* Get only language name */ + /* Get only language name. LANGINTKEY() already yields "" for an index that names no + language, and that empty name is what stops the callers' enumeration loop: a + placeholder here spins them forever. Contract in newlang.h. */ if (limit_to) { - if (strnotempty(hashname)) - strcpybuff(limit_to,hashname); - else - strcpybuff(limit_to,"???"); + strcpybuff(limit_to,hashname); return; } diff --git a/WinHTTrack/newlang.h b/WinHTTrack/newlang.h index f73fe09..b7508bd 100755 --- a/WinHTTrack/newlang.h +++ b/WinHTTrack/newlang.h @@ -28,6 +28,9 @@ Please visit our Website: http://www.httrack.com #ifndef HTS_DEFNEWLANG #define HTS_DEFNEWLANG +/* With limit_to, fetches only the current language's name into it, and leaves it + EMPTY when the index names no language. Callers enumerate the languages by walking + the index until that empty name, so never substitute a placeholder for it. */ void LANG_LOAD(char* limit_to); void LANG_INIT(); int LANG_T(int); From be27a4bb5b9ebbec41af7ebc7c580684a61f9e97 Mon Sep 17 00:00:00 2001 From: Xavier Roche Date: Sun, 26 Jul 2026 10:17:40 +0200 Subject: [PATCH 2/2] Review fixes: tighten the CI guard and cut the repeated comments The assertion accepted "after 0 entries", so a lang.def that loaded with no language keys would have passed it while proving nothing. Require at least one. The same contract was also spelled out four times over; keep it at the declaration and let the other three sites point at it. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_019HXNEMWtyrkW7xoD8ewcR3 Signed-off-by: Xavier Roche --- .github/workflows/windows-build.yml | 6 +++--- WinHTTrack/WinHTTrack.cpp | 5 ++--- WinHTTrack/newlang.cpp | 4 +--- WinHTTrack/newlang.h | 5 ++--- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml index a3acb2b..64ecdcf 100644 --- a/.github/workflows/windows-build.yml +++ b/.github/workflows/windows-build.yml @@ -646,9 +646,9 @@ jobs: if ("$so" -notmatch 'MBCS->UTF-8 ok') { throw "selftest did not exercise the MBCS->UTF-8 conversion" } - # The language list is walked until an empty name. Losing that terminator hung the - # first-run About box on every clean install, where nothing else here can reach. - if ("$so" -notmatch 'language list ends after \d+ entries') { + # Guards the empty-name terminator (newlang.h); losing it hangs the first-run About + # box. [1-9] not \d: "after 0 entries" would pass while proving nothing. + if ("$so" -notmatch 'language list ends after [1-9]\d* entries') { throw "selftest did not check that the language list terminates" } diff --git a/WinHTTrack/WinHTTrack.cpp b/WinHTTrack/WinHTTrack.cpp index c204124..eb30cf2 100755 --- a/WinHTTrack/WinHTTrack.cpp +++ b/WinHTTrack/WinHTTrack.cpp @@ -332,9 +332,8 @@ BOOL CWinHTTrackApp::InitInstance() printf("MBCS->UTF-8 ok\n"); } } - /* The language list is enumerated by walking the index until LANG_LOAD() reports an - empty name. When that terminator was lost the first-run About box spun forever, - and nothing else here could see it: --selftest exits long before that dialog. */ + /* Walk the languages as the About box does: losing LANG_LOAD()'s empty-name + terminator hangs it on the first run, past where --selftest ever reaches. */ { const int LANG_SANE_MAX = 512; /* lang.def ships a few dozen */ const int saved = QLANG_T(-1); diff --git a/WinHTTrack/newlang.cpp b/WinHTTrack/newlang.cpp index b04b02f..2c34131 100755 --- a/WinHTTrack/newlang.cpp +++ b/WinHTTrack/newlang.cpp @@ -325,9 +325,7 @@ void LANG_LOAD(char* limit_to) { hashname=LANGINTKEY(name); } - /* Get only language name. LANGINTKEY() already yields "" for an index that names no - language, and that empty name is what stops the callers' enumeration loop: a - placeholder here spins them forever. Contract in newlang.h. */ + /* Empty name is the caller's enumeration terminator: never substitute a placeholder. */ if (limit_to) { strcpybuff(limit_to,hashname); return; diff --git a/WinHTTrack/newlang.h b/WinHTTrack/newlang.h index b7508bd..d6056bc 100755 --- a/WinHTTrack/newlang.h +++ b/WinHTTrack/newlang.h @@ -28,9 +28,8 @@ Please visit our Website: http://www.httrack.com #ifndef HTS_DEFNEWLANG #define HTS_DEFNEWLANG -/* With limit_to, fetches only the current language's name into it, and leaves it - EMPTY when the index names no language. Callers enumerate the languages by walking - the index until that empty name, so never substitute a placeholder for it. */ +/* limit_to: set to the current language's name, or left EMPTY when the index names + none. Callers walk the index until that empty name, so never return a placeholder. */ void LANG_LOAD(char* limit_to); void LANG_INIT(); int LANG_T(int);