mirror of
https://github.com/xroche/httrack.git
synced 2026-07-18 14:50:32 +03:00
Compare commits
4 Commits
fix-630-pa
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d267aaf1bf | ||
|
|
226702fcab | ||
|
|
491774abda | ||
|
|
dbad05fcf0 |
2
.github/workflows/windows-build.yml
vendored
2
.github/workflows/windows-build.yml
vendored
@@ -188,7 +188,7 @@ jobs:
|
||||
|
||||
# Every gate here exits 77, so an all-skipped suite would report green having
|
||||
# tested nothing: pin the skips, and floor the passes in case the glob empties.
|
||||
expected_skips=" 48_local-crange-memresume.test" # pending #581
|
||||
expected_skips=" 48_local-crange-memresume.test 71_local-crange-repaircache.test" # pending #581
|
||||
[ "$pass" -ge 90 ] || { echo "::error::only $pass tests passed ($skip skipped)"; exit 1; }
|
||||
[ "$skipped" = "$expected_skips" ] || { echo "::error::unexpected skips:$skipped"; exit 1; }
|
||||
[ "$fail" -eq 0 ] || { echo "::error::failing:$failed"; exit 1; }
|
||||
|
||||
@@ -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("");
|
||||
|
||||
@@ -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".
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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); \
|
||||
|
||||
21
tests/01_zlib-repair-shift.test
Normal file
21
tests/01_zlib-repair-shift.test
Normal 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
|
||||
}
|
||||
90
tests/68_webhttrack-outdir-charset.test
Executable file
90
tests/68_webhttrack-outdir-charset.test
Executable 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"
|
||||
134
tests/71_local-crange-repaircache.test
Executable file
134
tests/71_local-crange-repaircache.test
Executable file
@@ -0,0 +1,134 @@
|
||||
#!/bin/bash
|
||||
# #581: an interrupted mirror can leave a truncated new.zip that pass 2 must
|
||||
# repair before resuming. The repaired-cache resume then hits the same hostile
|
||||
# 206 as test 48, so restart-whole must still drop the partial and refetch the
|
||||
# whole file. Pass 1 leaves a partial + temp-ref; we truncate new.zip past its
|
||||
# last local entry (dropping the central directory) so unzOpen fails and the
|
||||
# repair path runs; pass 2 resumes, rejects the range, and refetches whole.
|
||||
set -u
|
||||
|
||||
: "${top_srcdir:=..}"
|
||||
testdir=$(cd "$(dirname "$0")" && pwd)
|
||||
# shellcheck source=tests/testlib.sh
|
||||
. "${testdir}/testlib.sh"
|
||||
server=$(nativepath "${testdir}/local-server.py")
|
||||
root=$(nativepath "${testdir}/server-root")
|
||||
|
||||
python=$(find_python) || ! echo "python3 not found; skipping" >&2 || exit 77
|
||||
|
||||
# On Windows the pass-1 interrupt is a hard kill (MSYS can't signal a native
|
||||
# exe) and the restart-whole path fails on the repaired cache (#581) -- the very
|
||||
# bug this exercises; skip until the engine fix lands.
|
||||
if is_windows; then
|
||||
echo "Windows: restart-whole fails on a repaired cache (#581), skipping"
|
||||
exit 77
|
||||
fi
|
||||
|
||||
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/httrack_crangerep.XXXXXX") || exit 1
|
||||
serverpid=
|
||||
crawlpid=
|
||||
cleanup() {
|
||||
test -n "$crawlpid" && kill -9 "$crawlpid" 2>/dev/null
|
||||
stop_server "$serverpid"
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
trap cleanup EXIT HUP INT QUIT PIPE TERM
|
||||
|
||||
serverlog="${tmpdir}/server.log"
|
||||
: >"$serverlog"
|
||||
"$python" "$server" --root "$root" >"$serverlog" 2>&1 &
|
||||
serverpid=$!
|
||||
port=
|
||||
for _ in $(seq 1 50); do
|
||||
line=$(head -n1 "$serverlog" 2>/dev/null)
|
||||
if test "${line%% *}" == "PORT"; then
|
||||
port="${line#PORT }"
|
||||
break
|
||||
fi
|
||||
kill -0 "$serverpid" 2>/dev/null || {
|
||||
echo "server exited early: $(cat "$serverlog")"
|
||||
exit 1
|
||||
}
|
||||
sleep 0.1
|
||||
done
|
||||
test -n "$port" || {
|
||||
echo "could not discover server port"
|
||||
exit 1
|
||||
}
|
||||
base="http://127.0.0.1:${port}"
|
||||
|
||||
which httrack >/dev/null || {
|
||||
echo "could not find httrack"
|
||||
exit 1
|
||||
}
|
||||
out="${tmpdir}/crawl"
|
||||
mkdir "$out"
|
||||
common=(-O "$out" --quiet --disable-security-limits --robots=0 --timeout=30 --retries=1 -c1)
|
||||
refdir="${out}/hts-cache/ref"
|
||||
newzip="${out}/hts-cache/new.zip"
|
||||
|
||||
# --- pass 1: crawl, interrupt once the blob download is underway -------------
|
||||
printf '[pass 1: interrupt mid-download] ..\t'
|
||||
httrack "${common[@]}" "${base}/crange206mem/index.html" >"${tmpdir}/log1" 2>&1 &
|
||||
crawlpid=$!
|
||||
for _ in $(seq 1 300); do
|
||||
test -n "$(find "$refdir" -name '*.ref' 2>/dev/null)" && break
|
||||
kill -0 "$crawlpid" 2>/dev/null || break
|
||||
sleep 0.1
|
||||
done
|
||||
sleep 0.3
|
||||
kill -TERM "$crawlpid" 2>/dev/null
|
||||
wait "$crawlpid" 2>/dev/null
|
||||
crawlpid=
|
||||
test -n "$(find "$refdir" -name '*.ref' 2>/dev/null)" || {
|
||||
echo "FAIL: no temp-ref survived pass 1; cannot drive the resume"
|
||||
exit 1
|
||||
}
|
||||
echo "OK (temp-ref present)"
|
||||
|
||||
# --- damage the cache: drop the central directory a hard kill never wrote ----
|
||||
printf '[damage new.zip -> forces repair] ..\t'
|
||||
"$python" - "$newzip" <<'PY' || exit 1
|
||||
import os, sys
|
||||
path = sys.argv[1]
|
||||
data = open(path, "rb").read()
|
||||
cut = data.find(b"PK\x01\x02") # first central-directory header
|
||||
if cut <= 0:
|
||||
sys.exit("no central directory to drop in %s" % path)
|
||||
os.truncate(path, cut)
|
||||
PY
|
||||
echo "OK"
|
||||
|
||||
# --- pass 2: --continue -> repair -> resume -> hostile 206 -> refetch whole ---
|
||||
printf '[pass 2: repair, reject range, refetch] ..\t'
|
||||
rc=0
|
||||
httrack "${common[@]}" --continue "${base}/crange206mem/index.html" >"${tmpdir}/log2" 2>&1 || rc=$?
|
||||
test "$rc" -eq 0 || {
|
||||
echo "FAIL: httrack exited $rc"
|
||||
cat "${tmpdir}/log2" >&2
|
||||
exit 1
|
||||
}
|
||||
echo "OK (terminated)"
|
||||
|
||||
# The repair path must actually have run, else this is just a copy of test 48.
|
||||
printf '[cache repair fired] ..\t'
|
||||
grep -q 'damaged cache' "${out}/hts-log.txt" 2>/dev/null || {
|
||||
echo "FAIL: repair path did not run; damage was ineffective"
|
||||
exit 1
|
||||
}
|
||||
echo "OK"
|
||||
|
||||
blob=$(find "$out" -name blob.bin 2>/dev/null | head -1)
|
||||
full=6008 # len(CRANGE206_BODY) = len("CR206DAT") + 6000
|
||||
|
||||
printf '[file recovered whole] ..\t'
|
||||
test -s "$blob" || {
|
||||
echo "FAIL: blob.bin missing after the repaired-cache resume"
|
||||
exit 1
|
||||
}
|
||||
got=$(wc -c <"$blob")
|
||||
test "$got" -eq "$full" || {
|
||||
echo "FAIL: blob.bin is ${got} bytes, expected ${full}"
|
||||
exit 1
|
||||
}
|
||||
echo "OK (${got} bytes)"
|
||||
@@ -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 \
|
||||
@@ -149,6 +150,8 @@ TESTS = \
|
||||
65_port-siblings.test \
|
||||
66_engine-port80-strip.test \
|
||||
67_engine-delayed-truncate.test \
|
||||
69_local-intl-logdir.test
|
||||
68_webhttrack-outdir-charset.test \
|
||||
69_local-intl-logdir.test \
|
||||
71_local-crange-repaircache.test
|
||||
|
||||
CLEANFILES = check-network_sh.cache
|
||||
|
||||
Reference in New Issue
Block a user