Compare commits

...

6 Commits

Author SHA1 Message Date
Xavier Roche
c1430fa83f Merge origin/master into fix-629-webhttrack-argv-utf8
Resolve tests/Makefile.am: keep 66/67/69 (merged) alongside this branch's 68.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-18 09:45:01 +02:00
Xavier Roche
491774abda Signed-shift UB in zip-repair local-header read (#640)
unzRepair parses the local file header of a damaged cache zip during
repair. READ_32 combined two int-typed READ_16 halves as
READ_16(adr) | (READ_16((adr)+2) << 16); when the high half has bit 15
set (>= 0x8000), shifting it left by 16 exceeds INT_MAX, which is signed
overflow. UBSan aborts on the CRC/size fields of a header whose high
16-bit word has that bit set. Repair runs on hostile input (a corrupt or
foreign new.zip). Cast the halves to uLong before the shift, matching the
sibling minizip readers in unzip.c and zip.c.

Closes #639

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 09:41:30 +02:00
Xavier Roche
dbad05fcf0 Non-ASCII single -O drops hts-log.txt into a mangled twin directory on Windows
A single `-O` sets both `path_html` and `path_log`, so `httrack -O café url` sends the logs through `path_log` too. argv is UTF-8, so `path_log` holds UTF-8 bytes, but the two-file log branch in `hts_main_internal()` still created its directory and opened `hts-log.txt`/`hts-err.txt` through the ANSI `structcheck()`/`fopen()`. On Windows those read the bytes as the codepage and dropped the logs into a second, mangled `caf<mojibake>/` directory beside the mirror. This routes that branch through the same UTF-8 wrappers #628 used for the mirror root (`structcheck_utf8`/`FOPEN`/`UNLINK`/`fexist_utf8`), so the logs land under `café/` with the mirror. On a UTF-8 filesystem the wrappers resolve to the same calls, so only Windows changes.

This is the log half of #630. I left the cache half out on purpose: relocating `hts-cache` is a much larger, coupled change, because the cache is written through minizip's own ANSI `fopen` and cleaned up through an ANSI `opendir`, so half-converting it would split or break the cache rather than move it. That wants its own PR, and #630 should stay open for it.

Test 69 mirrors into a single non-ASCII `-O` and asserts the audits read `hts-log.txt` from that directory. Like test 64 it only bites on the Windows CI leg, where the two encodings differ; on Linux the change is a no-op and the whole suite stays green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 08:37:06 +02:00
Xavier Roche
4f32ec4109 Simplify the webhttrack outdir-charset test
Drop the origin server: only the decoded -O directory name is under
test, and htsserver creates it from the POST'd path before any fetch, so
a dead crawl target (port 1) exercises the same #629 path without a
second server and its poll loop. One htsserver, one poll, modelled on
the sibling webhttrack tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-18 08:25:14 +02:00
Xavier Roche
12f84b5182 Merge origin/master into fix-629-webhttrack-argv-utf8
Resolve tests/Makefile.am: keep 66_engine-port80-strip (#632) alongside this
branch's test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-18 08:03:17 +02:00
Xavier Roche
27ab7bc744 Convert webhttrack's form-charset POST body to UTF-8 argv
The httrack CLI and the WinHTTrack GUI both hand the engine UTF-8 argv,
the contract htsname and htscache now assume. webhttrack did not: its
back_launch_cmd splits the raw HTTP POST body into argv and calls
hts_main2 directly, and that body is in the form's declared
LANGUAGE_CHARSET (ISO-8859-1, windows-125x, BIG5, ... per language),
never UTF-8. A non-ASCII output path or URL from the web UI reached the
engine as raw form-charset bytes and was then decoded as UTF-8
downstream, landing the mirror in a mojibake directory.

Convert the command line from the current LANGUAGE_CHARSET to UTF-8
before launching, at the one point where that charset is known
(LANGSEL, right before webhttrack_main). ASCII and already-UTF-8 forms
pass through unchanged. webhttrack's own argv also gets the Windows
hts_argv_utf8 treatment the CLI has.

This exports hts_convertStringToUTF8 (an additive ABI change: new
symbol, soname unchanged), alongside the already-exported
hts_convertStringSystemToUTF8, so the server binary can reach it.

Closes #629

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-18 07:27:29 +02:00
12 changed files with 241 additions and 40 deletions

View File

@@ -373,7 +373,8 @@ char *hts_convertStringCPFromUTF8(const char *s, size_t size, UINT cp) {
return NULL;
}
char *hts_convertStringToUTF8(const char *s, size_t size, const char *charset) {
HTSEXT_API char *hts_convertStringToUTF8(const char *s, size_t size,
const char *charset) {
const UINT cp = hts_getCodepage(charset);
return hts_convertStringCPToUTF8(s, size, cp);
@@ -554,7 +555,8 @@ static char *hts_convertStringCharset(const char *s, size_t size,
return NULL;
}
char *hts_convertStringToUTF8(const char *s, size_t size, const char *charset) {
HTSEXT_API char *hts_convertStringToUTF8(const char *s, size_t size,
const char *charset) {
/* Empty string ? */
if (size == 0) {
return strdup("");

View File

@@ -51,8 +51,8 @@ typedef unsigned int hts_UCS4;
* Convert the string "s" from charset "charset" to UTF-8.
* Return NULL upon error.
**/
extern char *hts_convertStringToUTF8(const char *s, size_t size,
const char *charset);
HTSEXT_API char *hts_convertStringToUTF8(const char *s, size_t size,
const char *charset);
/**
* Convert the string "s" from UTF-8 to charset "charset".

View File

@@ -2368,30 +2368,28 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
opt->errlog = stderr;
} else if (httrack_logmode >= 2) {
// deux fichiers log
structcheck(StringBuff(opt->path_log));
if (fexist
(fconcat
(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt), StringBuff(opt->path_log), "hts-log.txt")))
remove(fconcat
(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt), StringBuff(opt->path_log), "hts-log.txt"));
if (fexist
(fconcat
(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt), StringBuff(opt->path_log), "hts-err.txt")))
remove(fconcat
(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt), StringBuff(opt->path_log), "hts-err.txt"));
// path_log holds UTF-8 bytes (argv is UTF-8): the ANSI file calls would
// read them as the codepage and drop the logs into a mangled twin (#630).
structcheck_utf8(StringBuff(opt->path_log));
if (fexist_utf8(fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
StringBuff(opt->path_log), "hts-log.txt")))
UNLINK(fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
StringBuff(opt->path_log), "hts-log.txt"));
if (fexist_utf8(fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
StringBuff(opt->path_log), "hts-err.txt")))
UNLINK(fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
StringBuff(opt->path_log), "hts-err.txt"));
/* Check FS directory structure created */
structcheck(StringBuff(opt->path_log));
structcheck_utf8(StringBuff(opt->path_log));
opt->log =
fopen(fconcat
(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt), StringBuff(opt->path_log), "hts-log.txt"),
"w");
opt->log = FOPEN(fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
StringBuff(opt->path_log), "hts-log.txt"),
"w");
if (httrack_logmode == 2)
opt->errlog =
fopen(fconcat
(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt), StringBuff(opt->path_log), "hts-err.txt"),
"w");
opt->errlog = FOPEN(fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
StringBuff(opt->path_log), "hts-err.txt"),
"w");
else
opt->errlog = opt->log;
if (opt->log == NULL) {

View File

@@ -2146,6 +2146,52 @@ static int st_cache_corrupt(httrackp *opt, int argc, char **argv) {
return err;
}
/* Drives unzRepair over a damaged local file header whose CRC field's high
16-bit word has bit 15 set. Before the READ_32 fix that shifted an int and
overflowed, so UBSan aborts here; after it, repair recovers the one entry. */
static int st_zip_repair_shift(httrackp *opt, int argc, char **argv) {
static const unsigned char zip[] = {
0x50, 0x4b, 0x03, 0x04, /* local file header signature */
0x14, 0x00, /* version needed */
0x00, 0x00, /* general purpose flag */
0x00, 0x00, /* method */
0x00, 0x00, /* time */
0x00, 0x00, /* date */
0x00, 0x00, 0xe8, 0x8a, /* crc: high word 0x8ae8, bit 15 set */
0x00, 0x00, 0x00, 0x00, /* compressed size */
0x00, 0x00, 0x00, 0x00, /* uncompressed size */
0x01, 0x00, /* filename length */
0x00, 0x00, /* extra field length */
0x61 /* filename "a" */
};
char in[HTS_URLMAXSIZE], out[HTS_URLMAXSIZE], tmp[HTS_URLMAXSIZE];
uLong nrec = 0, bytes = 0;
FILE *fp;
int err;
(void) opt;
if (argc < 1) {
fprintf(stderr, "zip-repair-shift: needs a directory\n");
return 1;
}
snprintf(in, sizeof(in), "%s/damaged.zip", argv[0]);
snprintf(out, sizeof(out), "%s/repair.zip", argv[0]);
snprintf(tmp, sizeof(tmp), "%s/repair.tmp", argv[0]);
fp = fopen(in, "wb");
if (fp == NULL || fwrite(zip, 1, sizeof(zip), fp) != sizeof(zip)) {
if (fp != NULL)
fclose(fp);
fprintf(stderr, "zip-repair-shift: cannot write %s\n", in);
return 1;
}
fclose(fp);
err = unzRepair(in, out, tmp, &nrec, &bytes);
printf("zip-repair-shift: %s (recovered %lu entr%s)\n",
(err == Z_OK && nrec == 1) ? "OK" : "FAIL", (unsigned long) nrec,
nrec == 1 ? "y" : "ies");
return (err == Z_OK && nrec == 1) ? 0 : 1;
}
static int st_cache_legacy(httrackp *opt, int argc, char **argv) {
int err;
@@ -3253,6 +3299,9 @@ static const struct selftest_entry {
st_cache_legacy},
{"cache-corrupt", "<dir>", "cache read-side corruption self-test",
st_cache_corrupt},
{"zip-repair-shift", "<dir>",
"cache zip-repair header read must not overflow a signed shift",
st_zip_repair_shift},
{"dns", "", "DNS resolver/cache self-test", st_dns},
{"dnstimeout", "", "a slow DNS resolve is bounded and holds no lock",
st_dnstimeout},

View File

@@ -40,6 +40,7 @@ Please visit our Website: http://www.httrack.com
#include "htsnet.h"
#include "htslib.h"
#include "htscharset.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -749,7 +750,14 @@ int smallserver(T_SOC soc, char *url, char *method, char *data, char *path) {
RUN THE SERVER
*/
if (strcmp((char *) adrcd, "start") == 0) {
webhttrack_main((char *) adr + p);
/* POST body is in the form's charset, not the
UTF-8 argv the engine now assumes (#629). */
char *const cmdl = (char *) adr + p;
char *cmdlUtf8 = hts_convertStringToUTF8(
cmdl, strlen(cmdl), LANGSEL("LANGUAGE_CHARSET"));
webhttrack_main(cmdlUtf8 != NULL ? cmdlUtf8 : cmdl);
freet(cmdlUtf8);
} else {
commandRunning = 0;
commandEnd = 1;

View File

@@ -65,6 +65,7 @@ Please visit our Website: http://www.httrack.com
#include "htsserver.h"
#include "htsurlport.h"
#include "htsweb.h"
#include "htscharset.h"
#if USE_BEGINTHREAD==0
#error fatal: no threads support
@@ -156,6 +157,7 @@ int main(int argc, char *argv[]) {
printf("Initializing the server..\n");
#ifdef _WIN32
hts_argv_utf8(&argc, &argv);
{
WORD wVersionRequested; // requested version WinSock API
WSADATA wsadata; // Windows Sockets API data

View File

@@ -14,7 +14,7 @@
#define READ_8(adr) ((unsigned char)*(adr))
#define READ_16(adr) ( READ_8(adr) | (READ_8(adr+1) << 8) )
#define READ_32(adr) ( READ_16(adr) | (READ_16((adr)+2) << 16) )
#define READ_32(adr) ((uLong) READ_16(adr) | ((uLong) READ_16((adr) + 2) << 16))
#define WRITE_8(buff, n) do { \
*((unsigned char*)(buff)) = (unsigned char) ((n) & 0xff); \

View File

@@ -0,0 +1,21 @@
#!/bin/bash
#
# Keep this POSIX-portable: the harness runs it via $(BASH), which is a plain
# POSIX /bin/sh on some platforms (e.g. macOS), so avoid bashisms and GNU-only
# tool flags despite the #!/bin/bash above.
# unzRepair header read must not overflow a signed shift (-#test=zip-repair-shift
# <dir>). A damaged local file header whose CRC high word has bit 15 set made
# READ_32 shift an int past INT_MAX; UBSan aborts before the fix casts to uLong.
set -eu
dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT
out=$(httrack -#test=zip-repair-shift "$dir")
printf '%s\n' "$out" | grep -qx "zip-repair-shift: OK (recovered 1 entry)" || {
echo "expected 'zip-repair-shift: OK (recovered 1 entry)', got: $out" >&2
exit 1
}

View File

@@ -0,0 +1,90 @@
#!/bin/bash
#
# webhttrack's POST body arrives in the form's declared charset, not UTF-8, yet
# the engine now assumes UTF-8 argv. A non-ASCII -O output dir submitted through
# the web UI must land under the UTF-8 directory, not an ISO-8859-1 twin (#629).
# Drives the real htsserver over HTTP; the default (English) form is served
# ISO-8859-1, so 'café' travels as the single byte 0xE9. The crawl target is a
# dead port: only the -O directory name is under test, and htsserver creates it
# from the decoded path before any fetch.
set -euo pipefail
testdir=$(cd "$(dirname "$0")" && pwd)
distdir=${top_srcdir:-$(cd "${testdir}/.." && pwd)}
distdir=$(cd "${distdir}" && pwd)
# shellcheck source=tests/testlib.sh
. "${testdir}/testlib.sh"
fail() {
echo "FAIL: $*" >&2
exit 1
}
command -v htsserver >/dev/null || fail "no htsserver in PATH"
python=$(find_python) || {
echo "python3 not found; skipping" >&2
exit 77
}
work=$(mktemp -d "${TMPDIR:-/tmp}/webhttrack_charset.XXXXXX") || fail "no tmpdir"
srvlog=$(mktemp)
srv=
cleanup() {
# htsserver keeps SIGTERM ignored across its exec, so only -9 reaps it.
test -z "${srv}" || kill -9 "${srv}" 2>/dev/null || true
wait "${srv}" 2>/dev/null || true # absorb bash's async "Killed" notice
rm -rf "${work}" "${srvlog}"
}
trap cleanup EXIT HUP INT QUIT PIPE TERM
# webhttrack server on a pre-picked port; an isolated HOME keeps a stray
# ~/.httrack.ini out of it.
sport=$("${python}" -c 'import socket
s = socket.socket()
s.bind(("127.0.0.1", 0))
print(s.getsockname()[1])
s.close()')
(
trap '' TERM TTOU
export HOME="${work}"
exec htsserver "${distdir}/" --port "${sport}" >"${srvlog}" 2>&1
) &
srv=$!
for _ in $(seq 1 40); do
url=$(sed -n 's/^URL=//p' "${srvlog}") && test -n "${url}" && break
kill -0 "${srv}" 2>/dev/null || break
sleep 0.25
done
test -n "${url:-}" || fail "htsserver did not start: $(cat "${srvlog}")"
# Post a "start" whose -O dir is 'café' in the form's ISO-8859-1 charset.
"${python}" - "${url}" "${work}" <<'PY'
import sys, urllib.parse, urllib.request
url, work = sys.argv[1], sys.argv[2]
outdir = work + "/caf\xe9" # 'café' as the single byte the browser would send
# Port 1 refuses at once: only the decoded -O dir matters, not the fetch.
cmd = "httrack --quiet --robots=0 http://127.0.0.1:1/x.html -O " + outdir
fields = [("path", work), ("projname", "proj"), ("command_do", "start"),
("winprofile", "x"), ("command", cmd)]
body = "&".join("%s=%s" % (k, urllib.parse.quote(v, safe="", encoding="latin-1"))
for k, v in fields)
req = urllib.request.Request(url + "step4.html", data=body.encode("latin-1"),
method="POST")
urllib.request.urlopen(req, timeout=20).read()
PY
# The crawl runs inside htsserver; wait for the -O dir, then check it chose the
# UTF-8 name and not the ISO-8859-1 twin.
utf8dir="${work}/café"
latin1dir="${work}/caf"$'\xe9'
for _ in $(seq 1 80); do
test -e "${utf8dir}" && break
kill -0 "${srv}" 2>/dev/null || break
sleep 0.25
done
test -e "${utf8dir}" || fail "-O dir never landed as UTF-8 café/: $(find "${work}" -maxdepth 2 2>/dev/null)"
test ! -e "${latin1dir}" || fail "-O dir created as the ISO-8859-1 twin instead"
echo "PASS"

15
tests/69_local-intl-logdir.test Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/bash
#
# A single non-ASCII -O sets both path_html and path_log to "café" (#630). The
# logs (hts-log.txt/hts-err.txt) must land there, not in an ANSI-mangled twin:
# on Windows path_log holds UTF-8 bytes the raw file calls read as the codepage.
# The --errors/--files/--log-found audits all grep logroot=café/hts-log.txt, so
# a log written to the twin fails them. POSIX has no twin, so this bites on the
# Windows CI leg (like test 64). The cache twin is a separate, larger fix.
: "${top_srcdir:=..}"
bash "$top_srcdir/tests/local-crawl.sh" --outdir-intl 'café' --errors 0 --files 5 \
--found 'simple/basic.html' \
--log-found 'mirror complete in' \
httrack 'BASEURL/simple/basic.html'

View File

@@ -84,6 +84,7 @@ TESTS = \
01_zlib-cache-legacy.test \
01_zlib-cache-golden.test \
01_zlib-cache-writefail.test \
01_zlib-repair-shift.test \
01_zlib-savename-cached.test \
02_manpage-regen.test \
02_update-cache.test \
@@ -148,6 +149,8 @@ TESTS = \
64_local-intl-outdir.test \
65_port-siblings.test \
66_engine-port80-strip.test \
67_engine-delayed-truncate.test
67_engine-delayed-truncate.test \
68_webhttrack-outdir-charset.test \
69_local-intl-logdir.test
CLEANFILES = check-network_sh.cache

View File

@@ -48,6 +48,7 @@ key="${testdir}/server.key"
tls=
verbose=
html_subdir=
outdir_intl=
rerun=
rerun_args=
rerun_dead=
@@ -140,6 +141,13 @@ while test "$pos" -lt "$nargs"; do
pos=$((pos + 1))
html_subdir="${args[$pos]}"
;;
--outdir-intl)
# Single non-ASCII -O "$out/NAME": path_html AND path_log are NAME, so
# the logs (and the harness reads of them) go through the non-ASCII path
# (#630). Distinct from --html-subdir, which keeps path_log ASCII.
pos=$((pos + 1))
outdir_intl="${args[$pos]}"
;;
--errors | --errors-content | --files)
audit+=("${args[$pos]}" "${args[$((pos + 1))]}")
pos=$((pos + 1))
@@ -216,13 +224,18 @@ test -n "$ver" || die "could not run httrack"
out="${tmpdir}/crawl"
mkdir "$out" || die "could not create $out"
# path_html holds the mirror + index; path_log holds hts-cache/hts-log.txt.
# Default: both are "$out". With --html-subdir, path_html becomes "$out/NAME"
# (the mirror root the audits inspect) while path_log stays "$out".
# Default: both are "$out". --html-subdir moves path_html to "$out/NAME" while
# path_log (logroot) stays "$out"; --outdir-intl moves both to "$out/NAME".
mirrorroot="$out"
logroot="$out"
odir="$out"
if test -n "$html_subdir"; then
mirrorroot="${out}/${html_subdir}"
odir="${mirrorroot},${out}"
elif test -n "$outdir_intl"; then
mirrorroot="${out}/${outdir_intl}"
logroot="$mirrorroot"
odir="$mirrorroot"
fi
# Localhost is fast; disable the rate/bandwidth safety limits but keep a
# max-time backstop so a hang cannot wedge the suite.
@@ -241,7 +254,7 @@ test "$crawlres" -eq 0 || ! result "httrack exited $crawlres" || {
exit 1
}
result "OK"
grep -iE "^[0-9:]*[[:space:]]Error:" "${out}/hts-log.txt" >&2
grep -iE "^[0-9:]*[[:space:]]Error:" "${logroot}/hts-log.txt" >&2
# --- optional second pass: re-mirror into the same dir (cache/update path) ----
if test -n "$rerun"; then
@@ -260,7 +273,7 @@ if test -n "$rerun"; then
# The update summary reports "files updated"; a fresh crawl never does. Assert
# it so a regression that bypasses the cache (re-crawls fresh) can't pass.
info "checking update used the cache"
if grep -aqE "mirror complete in .*files updated" "${out}/hts-log.txt"; then
if grep -aqE "mirror complete in .*files updated" "${logroot}/hts-log.txt"; then
result "OK"
else
result "update pass did not report cache activity"
@@ -292,7 +305,7 @@ if test -n "$rerun_dead"; then
zip="${out}/hts-cache/new.zip"
test -s "$zip" || die "no cache was written by the first pass"
cp "$zip" "${tmpdir}/cache-before.zip"
cp "${out}/hts-log.txt" "${tmpdir}/log-before.txt"
cp "${logroot}/hts-log.txt" "${tmpdir}/log-before.txt"
stop_server "$serverpid"
serverpid=
info "re-running httrack against the stopped server"
@@ -305,7 +318,7 @@ if test -n "$rerun_dead"; then
# The dead pass must have gone through the no-data rollback, not bailed out
# before the mirror loop (which would leave the cache trivially untouched).
info "checking the dead pass hit the rollback"
if grep -aq "No data seems to have been transferred" "${out}/hts-log.txt"; then
if grep -aq "No data seems to have been transferred" "${logroot}/hts-log.txt"; then
result "OK"
else
result "rollback notice not found in hts-log.txt"
@@ -320,7 +333,7 @@ if test -n "$rerun_dead"; then
exit 1
fi
# Audits below describe the healthy crawl, not the dead pass.
cp "${tmpdir}/log-before.txt" "${out}/hts-log.txt"
cp "${tmpdir}/log-before.txt" "${logroot}/hts-log.txt"
fi
# --- discover the single host root (127.0.0.1_<port> or 127.0.0.1) -----------
@@ -350,19 +363,19 @@ while test "$i" -lt "${#audit[@]}"; do
--errors)
i=$((i + 1))
assert_equals "checking errors" "${audit[$i]}" \
"$(grep -iEc "^[0-9:]*[[:space:]]Error:" "${out}/hts-log.txt")"
"$(grep -iEc "^[0-9:]*[[:space:]]Error:" "${logroot}/hts-log.txt")"
;;
--errors-content)
i=$((i + 1))
total=$(grep -icE "^[0-9:]*[[:space:]]Error:" "${out}/hts-log.txt")
total=$(grep -icE "^[0-9:]*[[:space:]]Error:" "${logroot}/hts-log.txt")
# transient network failures (statuscode -2..-6) flake on busy loopback;
# the code parens are followed by " at link" or " after N retries at link"
transient=$(grep -cE '\(-[2-6]\) (at link|after )' "${out}/hts-log.txt" || true)
transient=$(grep -cE '\(-[2-6]\) (at link|after )' "${logroot}/hts-log.txt" || true)
assert_equals "checking content errors" "${audit[$i]}" "$((total - transient))"
;;
--files)
i=$((i + 1))
nFiles=$(grep -E "^HTTrack Website Copier/[^ ]* mirror complete in " "${out}/hts-log.txt" |
nFiles=$(grep -E "^HTTrack Website Copier/[^ ]* mirror complete in " "${logroot}/hts-log.txt" |
sed -e 's/.*[[:space:]]\([^ ]*\)[[:space:]]files written.*/\1/g')
assert_equals "checking files" "${audit[$i]}" "$nFiles"
;;
@@ -393,7 +406,7 @@ while test "$i" -lt "${#audit[@]}"; do
--log-found)
i=$((i + 1))
info "checking log matches ${audit[$i]}"
if grep -aqE "${audit[$i]}" "${out}/hts-log.txt"; then result "OK"; else
if grep -aqE "${audit[$i]}" "${logroot}/hts-log.txt"; then result "OK"; else
result "not in log"
exit 1
fi
@@ -401,7 +414,7 @@ while test "$i" -lt "${#audit[@]}"; do
--log-not-found)
i=$((i + 1))
info "checking log lacks ${audit[$i]}"
if grep -aqE "${audit[$i]}" "${out}/hts-log.txt"; then
if grep -aqE "${audit[$i]}" "${logroot}/hts-log.txt"; then
result "present in log"
exit 1
else result "OK"; fi