Compare commits

..

3 Commits

Author SHA1 Message Date
Xavier Roche
a62bcbb47e Anchor the locale test's lift so it cannot silently stop testing
The lift ended at the first blank line, so a blank line grown inside the
locale block would truncate it: reinstating the old LANG-only bug below one
left the test passing 14/14 while the launcher returned English. Anchor on
the next section instead, and pin exported-empty as unset, which lang_of
cannot express.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-16 23:54:26 +02:00
Xavier Roche
24cb916562 Trim the locale test's comments
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-16 23:40:53 +02:00
Xavier Roche
f8235621c0 Fix webhttrack picking the wrong language from the locale
The launcher resolved LC_MESSAGES/LC_ALL/LANG into HTSLANG, then immediately
overwrote it with a value recomputed from $LANG alone, so LC_ALL and
LC_MESSAGES were ignored: a user with only LC_ALL=fr_FR.UTF-8 set got English.
That same line cut the region off with "cut -f1 -d'_'", leaving the zh_tw and
pt_br entries of lang.indexes unreachable, so Traditional Chinese fell back to
Simplified and Brazilian Portuguese to European Portuguese.

Resolve the locale in POSIX precedence order and look the tag up region-first,
falling back to the bare language. Also fix Slovenian's ISO 639-1 code, which
was "si" (Sinhala) in both lang.indexes and Slovenian.txt: sl_SI users got
English, and the served pages claimed lang="si".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-16 23:36:05 +02:00
7 changed files with 118 additions and 95 deletions

View File

@@ -23,7 +23,7 @@ pt:7
ro:25
ru:8
sk:20
si:24
sl:24
sv:17
tr:10
uk:22

View File

@@ -3,7 +3,7 @@ Slovenian
LANGUAGE_FILE
Slovenian
LANGUAGE_ISO
si
sl
LANGUAGE_AUTHOR
Jadran Rudec,iur.\r\njrudec@email.si \r\n
LANGUAGE_CHARSET

View File

@@ -113,7 +113,6 @@ HTSEXT_API int hts_main(int argc, char **argv) {
}
static int hts_main_internal(int argc, char **argv, httrackp * opt);
static hts_boolean cmdl_shortopt_has(const char *s, char c);
// Main, récupère les paramètres et appelle le robot
HTSEXT_API int hts_main2(int argc, char **argv, httrackp * opt) {
@@ -305,12 +304,12 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
hts_get_version_info(opt));
return 0;
} else {
if (strncmp(tmp_argv[0], "--", 2)) { /* not a long option */
if (cmdl_shortopt_has(tmp_argv[0], 'q'))
opt->quiet = HTS_TRUE; // never ask questions (nohup)
if (cmdl_shortopt_has(tmp_argv[0], 'i')) { // doit.log!
argv_url = -1;
opt->quiet = HTS_TRUE;
if (strncmp(tmp_argv[0], "--", 2)) { /* pas */
if ((strchr(tmp_argv[0], 'q') != NULL))
opt->quiet = 1; // ne pas poser de questions! (nohup par exemple)
if ((strchr(tmp_argv[0], 'i') != NULL)) { // doit.log!
argv_url = -1; /* forcer */
opt->quiet = 1;
}
} else if (strcmp(tmp_argv[0] + 2, "quiet") == 0) {
opt->quiet = 1; // ne pas poser de questions! (nohup par exemple)
@@ -2808,32 +2807,6 @@ int check_path(String * s, char *defaultname) {
return return_value;
}
/* Does the short-option cluster s carry c from the main option set (-i, -iC2,
-%Mi)? Walked as the parser does below: %, &, @ and # each take the letter
after them into another set, so the i of -%i is not the main-set -i. */
static hts_boolean cmdl_shortopt_has(const char *s, char c) {
const char *com;
if (s[0] != '-' || s[1] == '-')
return HTS_FALSE;
for (com = s + 1; *com != '\0'; com++) {
switch (*com) {
case '%':
case '&':
case '@':
case '#':
if (*(com + 1) != '\0')
com++; /* skip the other set's letter */
break;
default:
if (*com == c)
return HTS_TRUE;
break;
}
}
return HTS_FALSE;
}
// détermine si l'argument est une option
int cmdl_opt(char *s) {
if (s[0] == '-') { // c'est peut être une option

View File

@@ -33,6 +33,19 @@ function log {
return 0
}
# Map a POSIX locale ("zh_TW.UTF-8@euro") to its lang.indexes number, English (1) if unknown
function lang_index {
local locale=$1 indexes=$2 tag n t
tag=$(echo "${locale}" | cut -f1 -d'.' | cut -f1 -d'@' | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z_')
# a few languages carry a region-specific entry (zh_tw, pt_br); else use the bare language
for t in "${tag}" "${tag%%_*}"; do
n=$(grep -E "^${t}:" "${indexes}" | cut -f2 -d':')
test -n "${n}" && break
done
test -n "${n}" || n=1
echo "${n}"
}
function launch_browser {
log "Launching $1"
browser=$1
@@ -59,13 +72,11 @@ test -f "${DISTPATH}/lang.indexes" || ! log "Could not find ${DISTPATH}/lang.ind
test -d "${DISTPATH}/lang" || ! log "Could not find ${DISTPATH}/lang" || exit 1
test -d "${DISTPATH}/html" || ! log "Could not find ${DISTPATH}/html" || exit 1
# Locale
HTSLANG="${LC_MESSAGES}"
! test -n "${HTSLANG}" && HTSLANG="${LC_ALL}"
# Locale: POSIX precedence, LC_ALL overrides LC_MESSAGES overrides LANG
HTSLANG="${LC_ALL}"
! test -n "${HTSLANG}" && HTSLANG="${LC_MESSAGES}"
! test -n "${HTSLANG}" && HTSLANG="${LANG}"
HTSLANG="$(echo "$LANG" | cut -f1 -d'.' | cut -f1 -d'_')"
LANGN=$(grep -E "^${HTSLANG}:" "${DISTPATH}/lang.indexes" | cut -f2 -d':')
! test -n "${LANGN}" && LANGN=1
LANGN=$(lang_index "${HTSLANG}" "${DISTPATH}/lang.indexes")
# Find the browser
# note: not all systems have sensible-browser or www-browser alternative

View File

@@ -54,13 +54,6 @@ refused() {
! echo "FAIL: $1 (exit $RC)" || exit 1
}
# assert continue mode was entered: it drops the URL list, so with no cache to
# resume the run ends on the usage screen rather than on any other error
continued() {
{ test "$RC" -ne 0 && grep -q 'usage:' "$1/.log"; } ||
! echo "FAIL: $2 (exit $RC)" || exit 1
}
# a value past the old 126/256 caps but within the cap is accepted, on both the
# short and long form of each option
long=$(nchars 900)
@@ -109,50 +102,4 @@ for bad in nan nan:5 5:nan inf 10:5 99999; do
refused "#185: invalid --pause '$bad' not refused cleanly"
done
# An option is not -i (continue) merely because its name contains an 'i' (#615).
# These used to wipe the URL given before them and exit on the usage screen.
run "$tmp/ord-bti" --build-top-index
accepted "$tmp/ord-bti" "#615: --build-top-index after the URL wiped the URL list"
run "$tmp/ord-bti-s" "-%i"
accepted "$tmp/ord-bti-s" "#615: -%i after the URL wiped the URL list"
run "$tmp/ord-proto" --protocol 2
accepted "$tmp/ord-proto" "#615: --protocol after the URL wiped the URL list"
run "$tmp/ord-proto-s" "-@i2"
accepted "$tmp/ord-proto-s" "#615: -@i2 after the URL wiped the URL list"
# %, &, @ and # take the letter after them into another option set, wherever
# they sit in the cluster, so the i of -q%i is not the main-set -i either.
run "$tmp/ord-qpi" "-q%i"
accepted "$tmp/ord-qpi" "#615: -q%i after the URL wiped the URL list"
run "$tmp/ord-qai" "-q@i"
accepted "$tmp/ord-qai" "#615: -q@i after the URL wiped the URL list"
# -%q only forces quiet mode, which nothing here can see: a run whose stdout is
# not a tty is quiet from the start (htscoremain.c:174). Just check it crawls.
run "$tmp/ord-iqs" "-%q"
accepted "$tmp/ord-iqs" "#615: -%q after the URL broke the crawl"
# The real -i still forces continue mode and drops the URL list, in every form:
# a fix that merely stopped looking for 'i' would pass the checks above.
run "$tmp/cont-s" -i
continued "$tmp/cont-s" "#615: -i after the URL no longer forces continue mode"
run "$tmp/cont-c" -iC2
continued "$tmp/cont-c" "#615: -iC2 after the URL no longer forces continue mode"
run "$tmp/cont-l" --continue
continued "$tmp/cont-l" "#615: --continue after the URL no longer forces continue mode"
run "$tmp/cont-u" --update
continued "$tmp/cont-u" "#615: --update after the URL no longer forces continue mode"
# ...including where another set's letter precedes it in the cluster.
run "$tmp/cont-pq" "-%qi"
continued "$tmp/cont-pq" "#615: -%qi after the URL no longer forces continue mode"
run "$tmp/cont-pm" "-%Mi"
continued "$tmp/cont-pm" "#615: -%Mi after the URL no longer forces continue mode"
# Placed before the URL these always worked; they must keep working.
run_only "$tmp/pre-bti" "-%i" "file://$tmp/index.html"
accepted "$tmp/pre-bti" "#615: -%i before the URL broke the crawl"
run_only "$tmp/pre-proto" "-@i2" "file://$tmp/index.html"
accepted "$tmp/pre-proto" "#615: -@i2 before the URL broke the crawl"
exit 0

View File

@@ -0,0 +1,91 @@
#!/bin/bash
#
# webhttrack must map the caller's locale to the right lang.indexes number.
# Guards: LC_ALL/LC_MESSAGES were ignored, and zh_tw/pt_br were unreachable.
set -euo pipefail
testdir=$(cd "$(dirname "$0")" && pwd)
distdir=$(cd "${testdir}/.." && pwd)
script="${distdir}/src/webhttrack"
fail() {
echo "FAIL: $*" >&2
exit 1
}
test -r "${script}" || fail "no ${script}"
test -r "${distdir}/lang.indexes" || fail "no ${distdir}/lang.indexes"
# Lift the locale code out of the launcher, so this tests the shipped logic itself.
funcs=$(sed -n '/^function lang_index/,/^}/p' "${script}")
# End on the next section, not a blank line: a blank line grown inside the block
# would truncate the lift and silently stop testing everything below it.
grep -q '^# Find the browser' "${script}" || fail "locale block terminator moved in ${script}"
block=$(sed -n '/^# Locale/,/^# Find the browser/p' "${script}" | sed '$d')
echo "${block}" | grep -q 'LANGN=.*lang_index' || fail "could not lift the whole locale block from ${script}"
# Run the lifted block against whatever locale vars the caller exported.
run_block() {
# shellcheck disable=SC2034 # read by the eval'd locale block
DISTPATH="${distdir}"
# the launcher runs under neither -e ("! test -n x && y" returns 1) nor -u
set +eu
eval "${funcs}"
eval "${block}"
# shellcheck disable=SC2153 # LANGN is set by the eval'd block, not a LANG typo
echo "${LANGN}"
}
# Resolve LANG/LC_ALL/LC_MESSAGES (empty = unset) to the language number.
lang_of() {
(
unset LANG LC_ALL LC_MESSAGES
# only the value matters here; hide bash's setlocale gripe when the locale is not installed
# shellcheck disable=SC2030 # confining the locale to this subshell is the point
{
if test -n "$1"; then export LANG="$1"; fi
if test -n "$2"; then export LC_ALL="$2"; fi
if test -n "$3"; then export LC_MESSAGES="$3"; fi
} 2>/dev/null
run_block
)
}
check() {
local want=$1 got desc
shift
desc="LANG='$1' LC_ALL='$2' LC_MESSAGES='$3'"
got=$(lang_of "$1" "$2" "$3")
test "${got}" = "${want}" || fail "${desc}: want ${want}, got ${got}"
echo "ok: ${desc} -> ${got}"
}
# want LANG LC_ALL LC_MESSAGES
check 2 "fr_FR.UTF-8" "" ""
check 14 "zh_TW.UTF-8" "" "" # region-qualified: Chinese-BIG5, not Simplified (13)
check 12 "pt_BR.UTF-8" "" "" # region-qualified: Portugues-Brasil, not Portugues (7)
check 13 "zh_CN.UTF-8" "" "" # unlisted region falls back to the bare language
check 24 "sl_SI.UTF-8" "" "" # Slovenian is ISO 639-1 "sl"
check 4 "" "de_DE.UTF-8" "" # LC_ALL alone
check 4 "" "" "de_DE.UTF-8" # LC_MESSAGES alone
check 4 "fr_FR.UTF-8" "de_DE.UTF-8" "" # LC_ALL wins over LANG
check 4 "fr_FR.UTF-8" "" "de_DE.UTF-8" # LC_MESSAGES wins over LANG
check 2 "de_DE.UTF-8" "fr_FR.UTF-8" "de_DE.UTF-8" # LC_ALL wins over LC_MESSAGES
check 2 "fr_FR@euro" "" "" # @modifier without a charset
check 1 "C" "" "" # C, unknown and unset all mean English
check 1 "xx_YY.UTF-8" "" ""
check 1 "" "" ""
# Exported but empty is unset (POSIX), so it must not mask LANG. lang_of cannot
# express this: it exports nothing for an empty argument.
got=$(
unset LANG LC_ALL LC_MESSAGES
# shellcheck disable=SC2031 # confining the locale to this subshell is the point
{ export LANG="fr_FR.UTF-8" LC_ALL="" LC_MESSAGES=""; } 2>/dev/null
run_block
)
test "${got}" = 2 || fail "empty LC_ALL/LC_MESSAGES must not mask LANG: want 2, got ${got}"
echo "ok: LANG='fr_FR.UTF-8' with LC_ALL='' exported -> ${got}"
echo "PASS"

View File

@@ -136,6 +136,7 @@ TESTS = \
55_local-chunked.test \
56_local-proxy-noleak.test \
57_local-proxy-connect.test \
58_watchdog.test
58_watchdog.test \
61_webhttrack-locale.test
CLEANFILES = check-network_sh.cache