mirror of
https://github.com/xroche/httrack.git
synced 2026-08-09 09:18:30 +03:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0622bcd04 | ||
|
|
f5fbd1a82e | ||
|
|
496e85a3ac | ||
|
|
d4b77e53dc | ||
|
|
507f3565e8 | ||
|
|
4df1434b56 | ||
|
|
d85d91f8c6 | ||
|
|
b16ffc7be2 | ||
|
|
defd6735f3 | ||
|
|
178f9156e5 | ||
|
|
ee5db0a9e6 | ||
|
|
edeada647c | ||
|
|
562f6790ec | ||
|
|
cbb2257847 | ||
|
|
0e95c58865 | ||
|
|
8184b45696 | ||
|
|
62ca6e087a | ||
|
|
7c6e5c3018 | ||
|
|
1878595043 | ||
|
|
057648e335 | ||
|
|
7a7206d7f1 | ||
|
|
e4df3b387c | ||
|
|
58ddc5a6b1 | ||
|
|
fb01ad6f65 | ||
|
|
1a211555d0 | ||
|
|
f2abfc36ef | ||
|
|
b2cd2a51d2 | ||
|
|
562cd1c867 | ||
|
|
a2a20ef684 | ||
|
|
bfe0a4a7f7 | ||
|
|
03c30e17ce | ||
|
|
1a45e0237b | ||
|
|
2a44dc1728 | ||
|
|
23e182281c | ||
|
|
3426ea35ea | ||
|
|
0274e016af | ||
|
|
22fcf9ffff |
3
.gitattributes
vendored
3
.gitattributes
vendored
@@ -6,6 +6,3 @@
|
||||
# bash die on $'\r' on every line of them.
|
||||
*.test text eol=lf
|
||||
*.sh text eol=lf
|
||||
|
||||
# Scoped to this file so a union can never touch build logic.
|
||||
tests/tests-list.mk merge=union
|
||||
|
||||
100
.github/workflows/ci.yml
vendored
100
.github/workflows/ci.yml
vendored
@@ -41,7 +41,8 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
build-essential clang autoconf automake libtool autoconf-archive \
|
||||
zlib1g-dev libssl-dev libbrotli-dev libzstd-dev
|
||||
zlib1g-dev libssl-dev libbrotli-dev libzstd-dev \
|
||||
appstream appstream-util
|
||||
|
||||
- name: Configure
|
||||
run: |
|
||||
@@ -137,6 +138,66 @@ jobs:
|
||||
if: failure()
|
||||
run: cat tests/test-suite.log 2>/dev/null || true
|
||||
|
||||
# The Ubuntu buildds run a development release every runner above is well behind.
|
||||
# That gap is what shipped 3.49.18 broken on every Ubuntu architecture but
|
||||
# riscv64, when 25.10 made uutils the default coreutils (#1042).
|
||||
ubuntu-devel:
|
||||
name: build (Ubuntu devel, uutils coreutils)
|
||||
runs-on: ubuntu-24.04
|
||||
container: ubuntu:devel
|
||||
timeout-minutes: 40
|
||||
steps:
|
||||
# Before checkout: the container has no git, so submodules would be lost.
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
set -euo pipefail
|
||||
apt-get update
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
build-essential autoconf automake libtool autoconf-archive \
|
||||
zlib1g-dev libssl-dev libbrotli-dev libzstd-dev \
|
||||
git ca-certificates python3 procps
|
||||
|
||||
- name: Assert the coreutils dispatch on argv[0]
|
||||
# The property #1042 broke on, not the version banner, which uutils has
|
||||
# already reworded once. Without it the job quietly becomes a slower copy
|
||||
# of the one above.
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ln -s "$(command -v sleep)" /tmp/not-a-coreutil
|
||||
if /tmp/not-a-coreutil 0; then
|
||||
echo "::error::coreutils here take any argv[0]; this image no longer guards uutils" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Configure
|
||||
run: |
|
||||
set -euo pipefail
|
||||
autoreconf -fi
|
||||
./configure
|
||||
# A renamed dev package would disable a codec or TLS here, and the tests
|
||||
# that cover them would SKIP rather than fail.
|
||||
grep -q "define HTS_USEBROTLI 1" config.h
|
||||
grep -q "define HTS_USEZSTD 1" config.h
|
||||
grep -q "define HTS_USEOPENSSL 1" config.h
|
||||
|
||||
- name: Build
|
||||
run: make -j"$(nproc)"
|
||||
|
||||
- name: Test
|
||||
timeout-minutes: 25
|
||||
run: |
|
||||
set -euo pipefail
|
||||
jobs=$(( $(nproc) * 2 )); [ "$jobs" -le 16 ] || jobs=16
|
||||
make check -j"$jobs"
|
||||
|
||||
- name: Print the test log on failure
|
||||
if: failure()
|
||||
run: cat tests/test-suite.log 2>/dev/null || true
|
||||
|
||||
# Portability: build and test on macOS (Darwin/clang) on a native runner --
|
||||
# no VM. The tree has no __APPLE__ branches, so Darwin exercises the
|
||||
# generic-Unix path on a second libc and kernel. brew's openssl@3 is keg-only,
|
||||
@@ -510,6 +571,33 @@ jobs:
|
||||
UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1
|
||||
run: bash fuzz/run-fuzzers.sh fuzz check
|
||||
|
||||
# Catches #1030: the job above links ASan, whose runtime already defines the
|
||||
# sancov hooks the instrumentation calls, hiding a program left without one.
|
||||
fuzz-no-sanitizer:
|
||||
name: build (fuzzers, no sanitizer runtime, clang)
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
set -euo pipefail
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
build-essential clang autoconf automake libtool autoconf-archive \
|
||||
zlib1g-dev libssl-dev libbrotli-dev libzstd-dev
|
||||
|
||||
- name: Configure (fuzzers, static)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
autoreconf -fi
|
||||
./configure CC=clang --enable-fuzzers --disable-shared
|
||||
|
||||
- name: Build
|
||||
run: make -j"$(nproc)"
|
||||
|
||||
# Optional-dependency build: compile and test with HTTPS/OpenSSL disabled --
|
||||
# the configuration users on minimal systems build, and one libssl is not even
|
||||
# installed here so configure cannot silently re-enable it. The matrix above
|
||||
@@ -715,7 +803,8 @@ jobs:
|
||||
sudo apt-get update
|
||||
# noble ships shfmt 3.8.0 (universe), matching the pinned local dev
|
||||
# version; use it rather than fetching a release binary from github.com.
|
||||
sudo apt-get install -y --no-install-recommends shellcheck shfmt appstream
|
||||
sudo apt-get install -y --no-install-recommends shellcheck shfmt appstream \
|
||||
appstream-util
|
||||
shfmt --version
|
||||
appstreamcli --version
|
||||
|
||||
@@ -725,6 +814,11 @@ jobs:
|
||||
- name: shfmt
|
||||
run: shfmt -d -i 4 $SHELL_SCRIPTS
|
||||
|
||||
# A misnamed test falls outside the glob "make check" runs, so it goes
|
||||
# missing without failing anything.
|
||||
- name: test file names
|
||||
run: bash tests/check-test-names.sh
|
||||
|
||||
# MSBuild rejects a malformed .vcxproj with a bare MSB4025 and no build, so
|
||||
# catch it here in seconds rather than on a Windows runner minutes in.
|
||||
- name: XML well-formedness (MSBuild project files)
|
||||
@@ -742,6 +836,8 @@ jobs:
|
||||
set -euo pipefail
|
||||
appstreamcli validate --no-net --explain \
|
||||
html/server/div/com.httrack.WebHTTrack.metainfo.xml
|
||||
appstream-util validate-relax --nonet \
|
||||
html/server/div/com.httrack.WebHTTrack.metainfo.xml
|
||||
|
||||
# Check clang-format on CHANGED LINES ONLY. The engine predates clang-format
|
||||
# (it was shaped by an old Visual Studio formatter) and does not round-trip,
|
||||
|
||||
116
.github/workflows/cross-arch.yml
vendored
Normal file
116
.github/workflows/cross-arch.yml
vendored
Normal file
@@ -0,0 +1,116 @@
|
||||
# Cross-compiles for the Debian architectures no GitHub runner exists for.
|
||||
# 3.49.17 failed on four of them at once and every failure was a compile or
|
||||
# assemble error that x86-64 and arm64 cannot produce, so the buildd was the
|
||||
# first thing to see the code.
|
||||
name: Cross-arch
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
# Least privilege: the workflow only needs to read the repo.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# Cancel superseded runs on the same branch or PR.
|
||||
concurrency:
|
||||
group: cross-arch-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
cross:
|
||||
name: cross (${{ matrix.arch }})
|
||||
runs-on: ubuntu-24.04
|
||||
# sid, not stable: the point is to predict what the sid buildds will say.
|
||||
container: debian:sid
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# arch selects the hardening flags, triplet is the toolchain prefix.
|
||||
# The first four are 3.49.17's failures.
|
||||
- { arch: armhf, triplet: arm-linux-gnueabihf }
|
||||
- { arch: powerpc, triplet: powerpc-linux-gnu }
|
||||
- { arch: hppa, triplet: hppa-linux-gnu }
|
||||
- { arch: loong64, triplet: loongarch64-linux-gnu }
|
||||
- { arch: sh4, triplet: sh4-linux-gnu }
|
||||
- { arch: m68k, triplet: m68k-linux-gnu }
|
||||
- { arch: sparc64, triplet: sparc64-linux-gnu }
|
||||
- { arch: riscv64, triplet: riscv64-linux-gnu }
|
||||
- { arch: s390x, triplet: s390x-linux-gnu }
|
||||
env:
|
||||
DEB_HOST_ARCH: ${{ matrix.arch }}
|
||||
TRIPLET: ${{ matrix.triplet }}
|
||||
ZPREFIX: /tmp/zlib-${{ matrix.arch }}
|
||||
steps:
|
||||
- name: Install the toolchain
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# deb-src, so zlib below comes from the archive and not a pinned URL.
|
||||
# In place: a second stanza has to repeat Signed-By, and apt rejects
|
||||
# the same source carrying two spellings of the keyring.
|
||||
sed -i 's/^Types: deb$/Types: deb deb-src/' \
|
||||
/etc/apt/sources.list.d/debian.sources
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends \
|
||||
build-essential autoconf automake libtool autoconf-archive \
|
||||
dpkg-dev git ca-certificates \
|
||||
"gcc-$TRIPLET" "libc6-dev-$DEB_HOST_ARCH-cross"
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Cross-build zlib
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Mandatory, and no ports arch has a zlib1g-dev:<arch> to
|
||||
# multiarch-install. -fPIC because libhttrack.so links it.
|
||||
mkdir -p /tmp/zsrc && cd /tmp/zsrc
|
||||
# Unpatched: Debian's arch-specific patches expect its own rules to
|
||||
# build them (s390x's vector CRC pulls a header they add), and we only
|
||||
# need something to link -lz against.
|
||||
apt-get source --download-only zlib1g
|
||||
dpkg-source --skip-patches -x ./*.dsc src
|
||||
cd src
|
||||
CHOST="$TRIPLET" CC="$TRIPLET-gcc" CFLAGS="-O2 -fPIC" \
|
||||
./configure --prefix="$ZPREFIX" --static
|
||||
# configure detects s390x's vector CRC, whose sources live in the
|
||||
# contrib/ the .dfsg repack drops: drop the define and the object it
|
||||
# would build. Nothing here needs a fast CRC.
|
||||
sed -i -E 's/(-DHAVE_S390X_VX|crc32_vx\.l?o)//g' Makefile
|
||||
make -j"$(nproc)"
|
||||
make install
|
||||
|
||||
- name: Configure
|
||||
run: |
|
||||
set -euo pipefail
|
||||
autoreconf -fi
|
||||
mkdir -p /tmp/bld && cd /tmp/bld
|
||||
# The buildd's own per-arch flags: -D_FILE_OFFSET_BITS=64 is the shim
|
||||
# hazard, and -fstack-clash-protection is not offered everywhere.
|
||||
eval "$(dpkg-buildflags --export=sh)"
|
||||
# A cross AC_TRY_RUN answers "cross", which would swap in the bundled
|
||||
# snprintf that no buildd ever builds. Every glibc passes these.
|
||||
# --disable-https: no ports arch has a cross libssl, so the TLS paths
|
||||
# are the one part of the tree this matrix does not compile.
|
||||
"$GITHUB_WORKSPACE/configure" --host="$TRIPLET" \
|
||||
--build="$(dpkg-architecture -qDEB_BUILD_GNU_TYPE)" --disable-https \
|
||||
ac_cv_have_working_snprintf=yes ac_cv_have_working_vsnprintf=yes \
|
||||
CPPFLAGS="${CPPFLAGS:-} -I$ZPREFIX/include" \
|
||||
LDFLAGS="${LDFLAGS:-} -L$ZPREFIX/lib"
|
||||
|
||||
- name: Build
|
||||
run: make -C /tmp/bld -j"$(nproc)"
|
||||
|
||||
- name: Build the test artifacts
|
||||
# Nothing runs here, but the shims are where three of the four 3.49.17
|
||||
# failures were: they only build under `check`. Asserted, because an
|
||||
# empty TESTS= would otherwise pass having built nothing.
|
||||
run: |
|
||||
set -euo pipefail
|
||||
make -C /tmp/bld -j"$(nproc)" check TESTS=
|
||||
test -f /tmp/bld/tests/.libs/libaltstackprobe.so
|
||||
14
.github/workflows/windows-build.yml
vendored
14
.github/workflows/windows-build.yml
vendored
@@ -14,6 +14,9 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
# The suite watchdog reports as a commit status, the only channel that
|
||||
# outlives the runner it is reporting on (#795).
|
||||
statuses: write
|
||||
|
||||
# Cancel superseded runs on the same branch or PR.
|
||||
concurrency:
|
||||
@@ -37,6 +40,9 @@ jobs:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive # coucal lives in src/coucal
|
||||
# Or the job token stays in .git/config, where every test the suite
|
||||
# runs can read it.
|
||||
persist-credentials: false
|
||||
|
||||
# Located through vswhere rather than microsoft/setup-msbuild: the repo
|
||||
# only allows GitHub-owned actions.
|
||||
@@ -171,6 +177,14 @@ jobs:
|
||||
shell: bash
|
||||
working-directory: tests
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
# Through the environment, never argv, which the process list exposes.
|
||||
WATCHDOG_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
WATCHDOG_REPO: ${{ github.repository }}
|
||||
# github.sha here is the PR's merge commit, so statuses posted against it stay out of the PR's checks UI.
|
||||
WATCHDOG_SHA: ${{ github.sha }}
|
||||
WATCHDOG_CONTEXT: windows-suite (${{ matrix.platform }}, ${{ matrix.configuration }})
|
||||
WATCHDOG_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
run: |
|
||||
bash ./ci-windows-suite.sh \
|
||||
"$(cygpath -u "$GITHUB_WORKSPACE")/src/${{ matrix.platform }}/${{ matrix.configuration }}"
|
||||
|
||||
12
AGENTS.md
12
AGENTS.md
@@ -19,8 +19,12 @@ the operational checklist: toolchain, invariants, and how to ship a change.
|
||||
(`request_queue_size`) so macOS/BSD don't drop connections under a parallel
|
||||
`-c16` bigcrawl the way Python's default backlog of 5 did.
|
||||
Or run `sh build.sh` to do bootstrap + configure + make in one shot.
|
||||
- A `tests/NN_*.test` runs only if listed in `tests/tests-list.mk`; an
|
||||
unregistered file is silently skipped.
|
||||
- `configure` globs `TESTS` from `tests/[0-9]*_*.test`, so a new test needs no
|
||||
registration, but an existing build dir keeps the list it was configured with:
|
||||
`231_test-names.test` goes red until you reconfigure. It also compares the glob
|
||||
against `git ls-files`, so a test you forgot to `git add` fails there instead of
|
||||
quietly shrinking CI's suite. Name one outside the pattern and it never runs;
|
||||
`tests/check-test-names.sh` (also a CI lint) rejects that.
|
||||
- `make check` prepends the build's `src/` to `PATH`, but a hand-run `.test` does
|
||||
not — an installed `/usr/bin/httrack` then shadows your build. Run via `make
|
||||
check`, or `PATH="<bld>/src:$PATH"` for a manual run.
|
||||
@@ -38,6 +42,10 @@ the operational checklist: toolchain, invariants, and how to ship a change.
|
||||
match that is not on the last line is exposed. Capture the reply, assert the
|
||||
status line it must carry (an empty, truncated or redirected one is
|
||||
marker-free too), then match with a here-string: `grep -q M <<<"$reply"`.
|
||||
`head -c N` and `head -n N` end the same way, and there the reds are
|
||||
platform-specific: GNU `tail | head -c 3` survives, the uutils coreutils leg
|
||||
turns the same line into exit 141 with no output at all. Let the reader seek
|
||||
instead: `od -An -c -j <skip> -N <len> file`.
|
||||
|
||||
## Hard invariants
|
||||
- **Generated autotools files are NOT in git.** `configure`, every
|
||||
|
||||
34
configure.ac
34
configure.ac
@@ -1,6 +1,6 @@
|
||||
AC_PREREQ([2.71])
|
||||
|
||||
AC_INIT([httrack], [3.49.17], [roche+packaging@httrack.com], [httrack], [http://www.httrack.com/])
|
||||
AC_INIT([httrack], [3.49.19], [roche+packaging@httrack.com], [httrack], [http://www.httrack.com/])
|
||||
AC_COPYRIGHT([
|
||||
HTTrack Website Copier, Offline Browser for Windows and Unix
|
||||
Copyright (C) 1998-2015 Xavier Roche and other contributors
|
||||
@@ -29,6 +29,11 @@ AC_CONFIG_SRCDIR(src/httrack.c)
|
||||
AC_CONFIG_MACRO_DIR([m4])
|
||||
AC_CONFIG_HEADERS(config.h)
|
||||
AM_INIT_AUTOMAKE([subdir-objects])
|
||||
# 3:12:0: revision-only bump. httrackp gained a tail field (singlefile_state) and
|
||||
# htsopt.h added volatile qualifiers; no layout moved, nothing went away.
|
||||
# 3:11:0: revision-only bump. #1001 moved SOCaddr_inetntoa_ out of htsnet.h, adding
|
||||
# an export where callers had an inline; nothing changed or went away.
|
||||
# 3:10:0: revision-only bump; only the version macro moved, the engine is untouched.
|
||||
# 3:9:0: revision-only bump. #991 and #1005 each added an export
|
||||
# (hts_set_thread_hooks, escape_control_url); nothing changed or went away.
|
||||
# 3:8:0: revision-only bump, no ABI change.
|
||||
@@ -37,7 +42,7 @@ AM_INIT_AUTOMAKE([subdir-objects])
|
||||
# moves nothing). Soname stays .so.3: HTTrackQt is the only consumer of the installed
|
||||
# headers, so a libhttrack4 rename isn't worth it.
|
||||
# (3:0:0 was the htsblk mime-buffer widening, the ABI break that moved .so.2 -> .so.3.)
|
||||
VERSION_INFO="3:9:0"
|
||||
VERSION_INFO="3:12:0"
|
||||
AM_MAINTAINER_MODE
|
||||
AC_USE_SYSTEM_EXTENSIONS
|
||||
|
||||
@@ -392,7 +397,10 @@ AC_CHECK_TYPE(sa_family_t, [], [AC_DEFINE([sa_family_t], [uint16_t], [sa_family_
|
||||
AX_CHECK_ALIGNED_ACCESS_REQUIRED
|
||||
|
||||
# check for various headers
|
||||
AC_CHECK_HEADERS([execinfo.h sys/ioctl.h])
|
||||
AC_CHECK_HEADERS([execinfo.h sys/ioctl.h sys/random.h])
|
||||
|
||||
## CSPRNG for the --single-file mark; /dev/urandom is the fallback
|
||||
AC_CHECK_FUNCS([getrandom])
|
||||
|
||||
### zlib (mandatory)
|
||||
CHECK_ZLIB()
|
||||
@@ -547,7 +555,7 @@ AC_DEFINE(HAVE_STRNLEN, 1,[Check for strnlen])], AC_MSG_RESULT([not found]))
|
||||
## Online unit tests
|
||||
AC_MSG_CHECKING(whether to enable online unit tests)
|
||||
AC_ARG_ENABLE([online-unit-tests],
|
||||
[AS_HELP_STRING([--enable-online-unit-tests=@<:@yes/no/auto@:>@],[Enable online-unit-tests @<:@default=yes@:>@])],
|
||||
[AS_HELP_STRING([--enable-online-unit-tests=@<:@yes/no/auto@:>@],[Enable online-unit-tests @<:@default=no@:>@])],
|
||||
[
|
||||
case "${enableval}" in
|
||||
no|yes|auto)
|
||||
@@ -589,6 +597,10 @@ if test x"$fuzzers" = x"yes"; then
|
||||
AX_CHECK_COMPILE_FLAG([-fsanitize=fuzzer-no-link],
|
||||
[DEFAULT_CFLAGS="$DEFAULT_CFLAGS -fsanitize=fuzzer-no-link"],
|
||||
[AC_MSG_ERROR([--enable-fuzzers requires libFuzzer support (clang)])])
|
||||
# Ordinary programs are instrumented too, so they need that runtime at link.
|
||||
AX_CHECK_LINK_FLAG([-fsanitize=fuzzer-no-link],
|
||||
[DEFAULT_LDFLAGS="$DEFAULT_LDFLAGS -fsanitize=fuzzer-no-link"],
|
||||
[AC_MSG_ERROR([--enable-fuzzers requires libFuzzer support (clang)])])
|
||||
# clang's static sanitizer runtimes clash with -Wl,--no-undefined on the .so.
|
||||
if test x"$enable_shared" != x"no"; then
|
||||
AC_MSG_ERROR([--enable-fuzzers requires --disable-shared])
|
||||
@@ -596,6 +608,20 @@ if test x"$fuzzers" = x"yes"; then
|
||||
fi
|
||||
AM_CONDITIONAL([FUZZERS], [test x"$fuzzers" = x"yes"])
|
||||
|
||||
# tests/Makefile.am takes its TESTS from here instead of a checked-in list,
|
||||
# which made every pair of test-adding PRs conflict on the forge (#844).
|
||||
AC_MSG_CHECKING([for test scripts])
|
||||
TESTS_LIST=
|
||||
hts_ntests=0
|
||||
for hts_t in "$srcdir"/tests/[[0-9]]*_*.test; do
|
||||
test -f "$hts_t" || continue
|
||||
TESTS_LIST="$TESTS_LIST `echo "$hts_t" | sed 's|.*/||'`"
|
||||
hts_ntests=`expr $hts_ntests + 1`
|
||||
done
|
||||
AC_SUBST([TESTS_LIST])
|
||||
AC_MSG_RESULT([$hts_ntests])
|
||||
test "$hts_ntests" -gt 0 || AC_MSG_ERROR([no test script matched tests/[[0-9]]*_*.test])
|
||||
|
||||
# Final output
|
||||
AC_CONFIG_FILES([
|
||||
Makefile
|
||||
|
||||
22
debian/changelog
vendored
22
debian/changelog
vendored
@@ -1,3 +1,25 @@
|
||||
httrack (3.49.19-1) unstable; urgency=medium
|
||||
|
||||
* New upstream release: mostly FTP fixes, plus stack-overflow, mirror
|
||||
interruption and --update data-loss fixes; full list in history.txt. Two
|
||||
of them matter to the build: the test suite no longer assumes GNU
|
||||
coreutils, and the AppStream metainfo no longer declares a non-stock icon
|
||||
name that appstream-util rejects.
|
||||
|
||||
-- Xavier Roche <xavier@debian.org> Sat, 08 Aug 2026 21:29:56 +0200
|
||||
|
||||
httrack (3.49.18-1) unstable; urgency=medium
|
||||
|
||||
* New upstream release, fixing the 3.49.17-1 build failures on armhf,
|
||||
powerpc, hppa and loong64: the test suite's mmap interposer did not
|
||||
compile on the first three, and the alternate-stack test insisted on a
|
||||
longer backtrace than loong64's unwinder produces. Nothing in the engine
|
||||
changed. Upstream now cross-builds the test shims for the ports
|
||||
architectures on every push, so the next such break surfaces before the
|
||||
buildds do.
|
||||
|
||||
-- Xavier Roche <xavier@debian.org> Wed, 05 Aug 2026 23:00:24 +0200
|
||||
|
||||
httrack (3.49.17-1) unstable; urgency=medium
|
||||
|
||||
* New upstream release: security fixes in the FTP and HTTP request paths and
|
||||
|
||||
@@ -58,7 +58,9 @@ EXTRA_DIST = README.md run-fuzzers.sh \
|
||||
corpus/singlefile/img-src.html corpus/singlefile/link-rel.html \
|
||||
corpus/singlefile/style-block.html corpus/singlefile/style-attr.html \
|
||||
corpus/singlefile/srcset.html corpus/singlefile/rawtext.html \
|
||||
corpus/singlefile/malformed.html corpus/singlefile/many-attrs.html \
|
||||
corpus/singlefile/malformed.html \
|
||||
corpus/singlefile/mark-at-eof.html corpus/singlefile/mark-only.html \
|
||||
corpus/singlefile/mark-degenerate.html corpus/singlefile/traversal.html \
|
||||
corpus/sitemap/urlset.xml corpus/sitemap/sitemapindex.xml \
|
||||
corpus/sitemap/truncated.xml corpus/sitemap/urlset.xml.gz \
|
||||
corpus/arc/roundtrip.arc corpus/arc/truncated.arc \
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<img src="a.png">
|
||||
<img src=big.png alt=over-cap>
|
||||
<img src="../escape.png"><img src="/abs.png"><img src="data:,x">
|
||||
<img src="a.png">
|
||||
<img src=big.png alt=over-cap>
|
||||
<img src="../escape.png"><img src="/abs.png">
|
||||
<img src="data:,x">
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
<link rel="stylesheet" href="s.css">
|
||||
<link rel=icon href=a.png>
|
||||
<link rel="next" href="p2.html">
|
||||
<link rel="preload" href="j.js">
|
||||
<link rel="stylesheet" href="s.css">
|
||||
<link rel="icon" href="a.png">
|
||||
<link rel="canonical" href="other.html">
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
<img src="unterminated.png
|
||||
<div style="background:url(a.png">
|
||||
<style>@import url(
|
||||
<!-- unterminated comment
|
||||
<a href=
|
||||
<p a=1 b=2 c=3 d=4 e=5 f=6 g=7 h=8 i=9 j=10 title="> <img src=a.png>">x</p>
|
||||
<!--><img src="a.png">
|
||||
<img src="a.png" >
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<img src="a.png" a0="v" a1="v" a2="v" a3="v" a4="v" a5="v" a6="v" a7="v" a8="v" a9="v" a10="v" a11="v" a12="v" a13="v" a14="v" a15="v" a16="v" a17="v" a18="v" a19="v" a20="v" a21="v" a22="v" a23="v" a24="v" a25="v" a26="v" a27="v" a28="v" a29="v" a30="v" a31="v" a32="v" a33="v" a34="v" a35="v" a36="v" a37="v" a38="v" a39="v" a40="v" a41="v" a42="v" a43="v" a44="v" a45="v" a46="v" a47="v" a48="v" a49="v" a50="v" a51="v" a52="v" a53="v" a54="v" a55="v" a56="v" a57="v" a58="v" a59="v" a60="v" a61="v" a62="v" a63="v" a64="v" a65="v" a66="v" a67="v" a68="v" a69="v">
|
||||
1
fuzz/corpus/singlefile/mark-at-eof.html
Normal file
1
fuzz/corpus/singlefile/mark-at-eof.html
Normal file
@@ -0,0 +1 @@
|
||||
a.png
|
||||
5
fuzz/corpus/singlefile/mark-degenerate.html
Normal file
5
fuzz/corpus/singlefile/mark-degenerate.html
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
==
|
||||
""
|
||||
a.png#!htsinlin
|
||||
#!htsinlin
|
||||
1
fuzz/corpus/singlefile/mark-only.html
Normal file
1
fuzz/corpus/singlefile/mark-only.html
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
<script>var s="</scripting>"; if(a</b) x=1;</script>
|
||||
<script src="j.js"></script>
|
||||
<textarea></textareas></textarea>
|
||||
<title></titles></title>
|
||||
<script src="j.js"></script>
|
||||
<script>var u="a.png";</script>
|
||||
<textarea>a.png</textarea>
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
<img srcset="a.png 1x, big.png 2x, a.png 100w">
|
||||
<source srcset="a.png,, a.png 2x," src="a.png">
|
||||
<img srcset="a.png 1x, big.png 2x, a.png 480w">
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
<div style="background:url(a.png);list-style:url('a.png')"></div>
|
||||
<p style='background:url("a.png")'>x</p>
|
||||
<div style="background:url(a.png)"></div>
|
||||
<div style='background:url("a.png")'></div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<style>@import "s.css";
|
||||
@import url(sub/b.css);
|
||||
div{background:url(a.png)}
|
||||
/* url(a.png) */ p:after{content:"url(a.png)"}
|
||||
<style>@import "s.css";
|
||||
@import url(sub/b.css);
|
||||
div{background:url(a.png)}
|
||||
</style>
|
||||
|
||||
4
fuzz/corpus/singlefile/traversal.html
Normal file
4
fuzz/corpus/singlefile/traversal.html
Normal file
@@ -0,0 +1,4 @@
|
||||
<img src="../../../../etc/passwd">
|
||||
<img src="a.png#frag?q=1">
|
||||
<img src="sub/../a.png">
|
||||
<img src="./a.png">
|
||||
@@ -60,6 +60,10 @@ static void sf_cleanup(void) {
|
||||
(void) remove(sf_root);
|
||||
}
|
||||
|
||||
/* One httrackp for the whole run: the mark secret lives on it, so a fresh one
|
||||
per input would make every mark in the corpus unrecognisable. */
|
||||
static httrackp *sf_opt = NULL;
|
||||
|
||||
/* A missing asset would silently reduce the target to its parser half. */
|
||||
static void sf_write(const char *name, const char *data, size_t len) {
|
||||
char path[700];
|
||||
@@ -76,6 +80,48 @@ static void sf_text(const char *name, const char *data) {
|
||||
sf_write(name, data, strlen(data));
|
||||
}
|
||||
|
||||
/* Append ref plus a real mark for it. The secret is drawn per httrackp, so a
|
||||
fixture cannot spell one; every mark the target sees is built here. */
|
||||
static void sf_marked(String *out, const char *ref, char cls) {
|
||||
char mark[SINGLEFILE_MARK_MAX];
|
||||
|
||||
StringCat(*out, ref);
|
||||
StringCat(*out,
|
||||
singlefile_mark(sf_opt, mark, sizeof(mark), cls, strlen(ref)));
|
||||
}
|
||||
|
||||
/* \001<ref>\002 in an input becomes <ref> plus its mark, which is the only
|
||||
way a corpus file can reach the mark parser at all. */
|
||||
static void sf_expand_input(const char *in, size_t len, String *out) {
|
||||
size_t i, start = 0;
|
||||
|
||||
StringClear(*out);
|
||||
for (i = 0; i < len; i++) {
|
||||
if (in[i] == '\001') {
|
||||
start = StringLength(*out);
|
||||
} else if (in[i] == '\002') {
|
||||
char mark[SINGLEFILE_MARK_MAX];
|
||||
|
||||
StringCat(*out, singlefile_mark(sf_opt, mark, sizeof(mark),
|
||||
SINGLEFILE_CLASS_ANY,
|
||||
StringLength(*out) - start));
|
||||
} else {
|
||||
StringAddchar(*out, in[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void sf_css(const char *name, const char *pre, const char *ref,
|
||||
const char *post) {
|
||||
String body = STRING_EMPTY;
|
||||
|
||||
StringCopy(body, pre);
|
||||
sf_marked(&body, ref, SINGLEFILE_CLASS_ANY);
|
||||
StringCat(body, post);
|
||||
sf_write(name, StringBuff(body), StringLength(body));
|
||||
StringFree(body);
|
||||
}
|
||||
|
||||
static void sf_init(void) {
|
||||
static const char png[] = "\x89PNG\r\n\x1a\n";
|
||||
static const char big[4096] = "\x89PNG";
|
||||
@@ -83,6 +129,9 @@ static void sf_init(void) {
|
||||
char path[700];
|
||||
|
||||
hts_init();
|
||||
sf_opt = hts_create_opt();
|
||||
sf_opt->log = sf_opt->errlog = NULL;
|
||||
sf_opt->single_file_max_size = FUZZ_SF_CAP;
|
||||
snprintf(sf_root, sizeof(sf_root), "%s/httrack-fuzz-sf-XXXXXX",
|
||||
tmp != NULL && tmp[0] != '\0' ? tmp : "/tmp");
|
||||
if (mkdtemp(sf_root) == NULL)
|
||||
@@ -95,37 +144,50 @@ static void sf_init(void) {
|
||||
sf_write("a.png", png, sizeof(png) - 1);
|
||||
sf_write("big.png", big, sizeof(big));
|
||||
sf_text("j.js", "var x=1;\n");
|
||||
/* @import plus a url(), so an inlined stylesheet recurses and its own
|
||||
relative reference is rebased. */
|
||||
sf_text("s.css", "@import url(sub/b.css);\ndiv{background:url(a.png)}\n");
|
||||
sf_text("sub/b.css", "p{background:url(../a.png)}\n");
|
||||
/* Marked, so an inlined stylesheet recurses into its own marks and its
|
||||
un-inlinable reference is rebased; unmarked assets leave the target as a
|
||||
bare scan that reaches nothing. */
|
||||
{
|
||||
String css = STRING_EMPTY;
|
||||
|
||||
StringCopy(css, "@import url(");
|
||||
sf_marked(&css, "sub/b.css", SINGLEFILE_CLASS_CSS);
|
||||
StringCat(css, ");\ndiv{background:url(");
|
||||
sf_marked(&css, "a.png", SINGLEFILE_CLASS_ANY);
|
||||
StringCat(css, ")}\np{background:url(");
|
||||
sf_marked(&css, "big.png", SINGLEFILE_CLASS_ANY);
|
||||
StringCat(css, ")}\n");
|
||||
sf_write("s.css", StringBuff(css), StringLength(css));
|
||||
StringFree(css);
|
||||
}
|
||||
sf_css("sub/b.css", "p{background:url(", "../a.png", ")}\n");
|
||||
}
|
||||
|
||||
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||
static int inited = 0;
|
||||
|
||||
String out = STRING_EMPTY;
|
||||
httrackp *opt;
|
||||
/* Exact-length, unterminated: the rewriter is span-based, so ASan bounds a
|
||||
read past html_len instead of it landing on a terminator. */
|
||||
char *html = malloct(size != 0 ? size : 1);
|
||||
String in = STRING_EMPTY;
|
||||
char *html;
|
||||
size_t html_len;
|
||||
|
||||
if (!inited) {
|
||||
sf_init();
|
||||
inited = 1;
|
||||
}
|
||||
memcpy(html, data, size);
|
||||
|
||||
opt = hts_create_opt();
|
||||
opt->log = opt->errlog = NULL;
|
||||
opt->single_file_max_size = FUZZ_SF_CAP;
|
||||
sf_expand_input((const char *) data, size, &in);
|
||||
html_len = StringLength(in);
|
||||
/* Exact-length, unterminated: the rewriter is span-based, so ASan bounds a
|
||||
read past html_len instead of it landing on a terminator. */
|
||||
html = malloct(html_len != 0 ? html_len : 1);
|
||||
memcpy(html, StringBuff(in), html_len);
|
||||
StringFree(in);
|
||||
|
||||
StringClear(out);
|
||||
(void) singlefile_rewrite_html(opt, sf_root, sf_page, html, size,
|
||||
(void) singlefile_rewrite_html(sf_opt, sf_root, sf_page, html, html_len,
|
||||
SINGLEFILE_MAX_PAGE_SIZE, &out);
|
||||
|
||||
StringFree(out);
|
||||
freet(html);
|
||||
hts_free_opt(opt);
|
||||
return 0;
|
||||
}
|
||||
|
||||
27
history.txt
27
history.txt
@@ -4,6 +4,33 @@ HTTrack Website Copier release history:
|
||||
|
||||
This file lists all changes and fixes that have been made for HTTrack
|
||||
|
||||
3.49-19
|
||||
+ Fixed: an over-long user name in an ftp:// URL was truncated at the credential boundary, so the login went to a different account (#1032)
|
||||
+ Fixed: an over-long FTP path or host name aborted the process instead of failing that one link (#1019)
|
||||
+ Fixed: an FTP crawl ignored --timeout and --max-time, so a server that went quiet held a slot for five minutes (#1039)
|
||||
+ Fixed: an FTP crawl of a dead host took two minutes to give up, and no signal could shorten it (#1059)
|
||||
+ Fixed: the engine released the transfer slots while an FTP thread was still writing to one, crashing a mirror that hit its size or time limit (#1051)
|
||||
+ Fixed: Ctrl-C did not shorten a connection or a name lookup already in flight, so ending a crawl against an unreachable host took minutes (#1073)
|
||||
+ Fixed: a crawl that gave up mid-loop, on the link limit or an abort, lost its WARC archive and its change report, and a WARC written past teardown was stranded in a .tmp file (#1061, #1060)
|
||||
+ Fixed: a crawl that rolled back for want of data still replaced the previous WARC archive, stranding its temporary and never writing its index; a stale or unwritable index is now reported rather than left silently in place (#1053)
|
||||
+ Fixed: a resume the server answered with an unusable Content-Range deleted the partial file and never fetched it again; the restart no longer spends the retry budget, and is latched to one per link (#581, #1052)
|
||||
+ Fixed: a long argument to -n or -O smashed the stack through an unbounded panic message, and the growable arrays could wrap their capacity to zero (#1040)
|
||||
+ Fixed: --single-file could write a reference mark longer than it reads back, leaving it in the delivered page as visible text (#1054, #1069)
|
||||
+ Fixed: on a terminal with no input left, a crawl toggled between the spinner and the full panel on every pass (#1072)
|
||||
+ Fixed: -%v1 ended each line with a carriage return, so the lines overwrote each other and a redirected log was unreadable; -%v2 now paints its panel only on a terminal
|
||||
+ Fixed: the installed socket headers did not compile under -std=c99 without _POSIX_C_SOURCE (#1001)
|
||||
+ Fixed: the 16 px application icon rendered every stem as grey (#938)
|
||||
+ Fixed: the desktop files installed outside the data directory, and the AppStream metainfo declared an icon name that is not a stock one
|
||||
+ Fixed: the build failed on distributions whose coreutils are not GNU's (#1042)
|
||||
+ Changed: --single-file drops its own HTML and CSS scanner and reuses the crawler's parser, so it follows the same references the mirror does (#749)
|
||||
+ Changed: updated Turkish translation, and repaired two mangled Polish strings (#306)
|
||||
+ Changed: multiple internal hardening, build, test and CI improvements
|
||||
|
||||
3.49-18
|
||||
+ Fixed: the 3.49.17 package build failed on four architectures: the test suite's mmap interposer did not compile on armhf, powerpc or hppa, and the alternate-stack test rejected loong64's shorter backtrace (#1023)
|
||||
+ Fixed: the test suite failed on a host with no ps command, such as a Fedora build root; its hang diagnostics now read /proc directly (#1021)
|
||||
+ Changed: multiple internal test and CI improvements, including a cross-compile matrix covering the Debian ports architectures
|
||||
|
||||
3.49-17
|
||||
+ New: a pkg-config file, libhttrack.pc, ships with the development headers (#1018)
|
||||
+ Fixed: a crawled page could read the WebHTTrack session id and drive the control panel; the id is now unguessable and cross-origin commands are refused (#877)
|
||||
|
||||
@@ -15,7 +15,7 @@ WebIcon64x64dir = $(datadir)/icons/hicolor/64x64/apps
|
||||
WebIcon128x128dir = $(datadir)/icons/hicolor/128x128/apps
|
||||
WebIcon256x256dir = $(datadir)/icons/hicolor/256x256/apps
|
||||
WebIconScalabledir = $(datadir)/icons/hicolor/scalable/apps
|
||||
VFolderEntrydir = $(prefix)/share/applications
|
||||
VFolderEntrydir = $(datadir)/applications
|
||||
MetaInfodir = $(datadir)/metainfo
|
||||
|
||||
# Wildcards are globbed against $(srcdir): a bare "*.html" is resolved against
|
||||
|
||||
@@ -155,6 +155,8 @@ rather than on <tt>url</tt> when a mirror is known to carry legacy-charset URLs.
|
||||
|
||||
<h3 id="example">Example</h3>
|
||||
|
||||
One captured run; <tt>generator</tt> and <tt>date</tt> will read differently in yours.
|
||||
|
||||
<pre>
|
||||
{
|
||||
"schema": 1,
|
||||
|
||||
@@ -58,6 +58,9 @@
|
||||
|
||||
<main id="main">
|
||||
|
||||
<!-- Section ids are link API: the redirect stubs and the Android app's contextual help
|
||||
hardcode them, and an app release outlives its engine pin. -->
|
||||
|
||||
<h1>The HTTrack interface, step by step</h1>
|
||||
|
||||
<p class="lede">HTTrack copies a website to your disk so you can read it offline. The same
|
||||
@@ -88,7 +91,7 @@ itself down once those pings stop.</p>
|
||||
<p>Each option also lists its command-line equivalent, so anything you set here can later be
|
||||
scripted. The <a href="cmdguide.html">command-line guide</a> covers that side.</p>
|
||||
|
||||
<div class="note" data-for="droid">
|
||||
<div class="note" data-for="droid" id="droid-first-launch">
|
||||
<p><b>First launch.</b> Android asks for permission to store mirrors on your device. Without
|
||||
it the app cannot save anything, so tap <b>Allow</b>.</p>
|
||||
<figure>
|
||||
@@ -287,7 +290,7 @@ usual causes.</p>
|
||||
<p data-for="win web">The files sit under the base path you chose, in a folder named after the
|
||||
project. Opening <code>index.html</code> there browses the mirror without HTTrack.</p>
|
||||
|
||||
<p data-for="droid">Mirrors are written to
|
||||
<p data-for="droid" id="droid-storage-location">Mirrors are written to
|
||||
<code>/storage/emulated/0/HTTrack/Websites</code>, in a folder named after the project. That is
|
||||
shared storage, so a file manager or a USB cable can reach it, and opening
|
||||
<code>index.html</code> there browses the mirror without the app.</p>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 447 B After Width: | Height: | Size: 98 B |
@@ -26,7 +26,6 @@
|
||||
</ul>
|
||||
</description>
|
||||
<launchable type="desktop-id">WebHTTrack.desktop</launchable>
|
||||
<icon type="stock">httrack</icon>
|
||||
<categories>
|
||||
<category>Network</category>
|
||||
</categories>
|
||||
@@ -51,6 +50,27 @@
|
||||
<content_rating type="oars-1.1"/>
|
||||
<!-- Newest first; tests/01_engine-version-macros.test enforces it. -->
|
||||
<releases>
|
||||
<release version="3.49.19" date="2026-08-08">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Stopping a mirror now takes effect at once, instead of waiting for connections that were still being opened</li>
|
||||
<li>A mirror that stops early keeps its web archive and its list of changes</li>
|
||||
<li>A partly downloaded file the server refuses to resume is fetched again rather than lost</li>
|
||||
<li>An FTP site that stops responding no longer blocks a download for five minutes</li>
|
||||
<li>A very long web address or FTP path no longer crashes the mirror</li>
|
||||
<li>Stopping a mirror while an FTP file is downloading no longer crashes</li>
|
||||
<li>An FTP address carrying an over-long user name could sign in as a different account</li>
|
||||
<li>Updated Turkish translation</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="3.49.18" date="2026-08-05">
|
||||
<description>
|
||||
<ul>
|
||||
<li>The program itself is unchanged: this release only fixes building HTTrack from source on some Linux distributions and processors</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="3.49.17" date="2026-08-05">
|
||||
<description>
|
||||
<ul>
|
||||
|
||||
@@ -1,37 +1,24 @@
|
||||
/* XPM */
|
||||
static char *httrack16x16[] = {
|
||||
/* columns rows colors chars-per-pixel */
|
||||
"16 16 15 1 ",
|
||||
" c #0D0D10",
|
||||
". c #191921",
|
||||
"X c #2A2A38",
|
||||
"o c #363647",
|
||||
"O c #424257",
|
||||
"+ c #53536E",
|
||||
"@ c #717197",
|
||||
"# c #7E7EA8",
|
||||
"$ c #8585B1",
|
||||
"% c #9898CB",
|
||||
"& c #9999CC",
|
||||
"* c #9A9ACE",
|
||||
"= c #9696C9",
|
||||
"- c #9C9CD0",
|
||||
"; c #9999CC",
|
||||
"16 16 2 1 ",
|
||||
" c #040404",
|
||||
". c #9999CC",
|
||||
/* pixels */
|
||||
"&&&&&&&&&%%&&&&&",
|
||||
"&&&&&&&&%%%%&&&&",
|
||||
"&&&&&&&&%%%%&&&&",
|
||||
"&%%&&&%%%%%%%%%&",
|
||||
"%+o$&%+o@ooooo+%",
|
||||
"%o $-%X @X. .O%",
|
||||
"%o @$$X $=@ o$=&",
|
||||
"%o . $-# o&&&",
|
||||
"%o oOo. $-# o%&&",
|
||||
"%o $-%X $-# o%&&",
|
||||
"%X $&%X $-# o%&&",
|
||||
"%+o$&%+o$&$o+%&&",
|
||||
"%%%&&&%%&&&%%&&&",
|
||||
"%%&&&&&&&&&&&&&&",
|
||||
"%&&&&&&&&&&&&&&&",
|
||||
"&&&&&&&&&&&&&&&&"
|
||||
"................",
|
||||
"................",
|
||||
"................",
|
||||
"................",
|
||||
". ... . .",
|
||||
". ... . .",
|
||||
". ... ... ...",
|
||||
". ... ...",
|
||||
". ... ...",
|
||||
". ... ... ...",
|
||||
". ... ... ...",
|
||||
". ... ... ...",
|
||||
"................",
|
||||
"................",
|
||||
"................",
|
||||
"................"
|
||||
};
|
||||
|
||||
@@ -539,9 +539,9 @@ U
|
||||
TimeOut(s)
|
||||
Przekroczenie(a) czasu oczekiwania
|
||||
Persistent connections (Keep-Alive)
|
||||
Podtrzymywane połączenia (Keep-Alive)
|
||||
Podtrzymywane po³¹czenia (Keep-Alive)
|
||||
Reduce connection time and type lookup time using persistent connections
|
||||
Zmniejsz czas połączenia i czas sprawdzania typu dzięki użyciu podtrzymywanych połączeń
|
||||
Zmniejsz czas po³¹czenia i czas sprawdzania typu dziêki u¿yciu podtrzymywanych po³¹czeñ
|
||||
Retries
|
||||
Ponowienia
|
||||
Size limit
|
||||
|
||||
454
lang/Turkish.txt
454
lang/Turkish.txt
File diff suppressed because it is too large
Load Diff
@@ -29,8 +29,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
========================================================================
|
||||
MAKEFILE PROJECT : libtest Project Overview
|
||||
|
||||
@@ -43,7 +43,8 @@ htsserver_LDADD = $(THREADS_LIBS) $(SOCKET_LIBS) libhttrack.la
|
||||
proxytrack_LDADD = $(THREADS_LIBS) $(SOCKET_LIBS)
|
||||
|
||||
httrack_CFLAGS = $(AM_CFLAGS) $(CFLAGS_PIE)
|
||||
proxytrack_CFLAGS = $(AM_CFLAGS) $(CFLAGS_PIE) -DNO_MALLOCT -DZLIB_CONST -DHTS_INTHASH_USES_MD5
|
||||
# -DHTS_NO_LIBHTTRACK: see htsnet.h's HTSNET_API guard.
|
||||
proxytrack_CFLAGS = $(AM_CFLAGS) $(CFLAGS_PIE) -DNO_MALLOCT -DZLIB_CONST -DHTS_INTHASH_USES_MD5 -DHTS_NO_LIBHTTRACK
|
||||
htsserver_CFLAGS = $(AM_CFLAGS) $(CFLAGS_PIE) -DZLIB_CONST -DHTS_INTHASH_USES_MD5
|
||||
|
||||
# @RPATH_ORIGIN_LDFLAGS@ makes a copied tree find libhttrack next to it (#906),
|
||||
@@ -55,11 +56,12 @@ htsserver_LDFLAGS = $(AM_LDFLAGS) $(LDFLAGS_PIE) @RPATH_ORIGIN_LDFLAGS@
|
||||
lib_LTLIBRARIES = libhttrack.la
|
||||
|
||||
htsserver_SOURCES = htsserver.c htsserver.h htsweb.c htsweb.h htsstats.h \
|
||||
htsrandom.c htsrandom.h \
|
||||
htscmdline.c htscmdline.h \
|
||||
htsurlport.c htsurlport.h
|
||||
proxytrack_SOURCES = proxy/main.c \
|
||||
proxy/proxytrack.c proxy/store.c \
|
||||
htsurlport.c htsurlport.h \
|
||||
htsurlport.c htsurlport.h htsnet.c \
|
||||
coucal/coucal.c htsmd5.c md5.c \
|
||||
minizip/ioapi.c minizip/mztools.c minizip/unzip.c minizip/zip.c
|
||||
|
||||
@@ -77,10 +79,10 @@ libhttrack_la_SOURCES = htscore.c htsparse.c htsback.c htscache.c \
|
||||
htscache_selftest.c htsdns_selftest.c htsselftest.c \
|
||||
htscatchurl.c htsfilters.c htsftp.c htshash.c coucal/coucal.c \
|
||||
htscmdline.c htshelp.c htslib.c htsurlport.c htscoremain.c \
|
||||
htsname.c htsrobots.c htstools.c htswizard.c \
|
||||
htsname.c htsrobots.c htstools.c htsrandom.c htswizard.c \
|
||||
htsalias.c htsthread.c htsindex.c htsbauth.c \
|
||||
htscrashtest.c \
|
||||
htsmd5.c htscodec.c htswarc.c htschanges.c htssinglefile.c htssitemap.c htsproxy.c htszlib.c htswrap.c htsconcat.c \
|
||||
htsmd5.c htsnet.c htscodec.c htswarc.c htschanges.c htssinglefile.c htssitemap.c htsproxy.c htszlib.c htswrap.c htsconcat.c \
|
||||
htsmodules.c htscharset.c punycode.c htsencoding.c htssniff.c \
|
||||
md5.c \
|
||||
minizip/ioapi.c minizip/mztools.c minizip/unzip.c minizip/zip.c \
|
||||
@@ -91,7 +93,7 @@ libhttrack_la_SOURCES = htscore.c htsparse.c htsback.c htscache.c \
|
||||
htshelp.h htsindex.h htslib.h htsurlport.h htsmd5.h \
|
||||
htsmodules.h htsname.h htsnet.h htssniff.h \
|
||||
htsopt.h htsrobots.h htsthread.h \
|
||||
htscrashtest.h htstools.h htswizard.h htswrap.h htscodec.h htswarc.h htschanges.h htssinglefile.h htssitemap.h htsproxy.h htszlib.h \
|
||||
htscrashtest.h htstools.h htsrandom.h htswizard.h htswrap.h htscodec.h htswarc.h htschanges.h htssinglefile.h htssitemap.h htsproxy.h htszlib.h \
|
||||
htsstrings.h htsarrays.h httrack-library.h \
|
||||
htscharset.h punycode.h htsencoding.h \
|
||||
htsentities.h htsentities.sh htsbasiccharsets.sh htscodepages.h \
|
||||
|
||||
@@ -106,18 +106,26 @@ static HTS_UNUSED void hts_record_assert_memory_failed(const size_t size) {
|
||||
/**
|
||||
* Ensure at least 'ROOM' elements can be put in the remaining space.
|
||||
* After a call to this macro, TypedArrayRoom(A) is guaranteed to be at
|
||||
* least equal to 'ROOM'.
|
||||
* least equal to 'ROOM'. Aborts if the total would not fit a size_t.
|
||||
**/
|
||||
#define TypedArrayEnsureRoom(A, ROOM) \
|
||||
do { \
|
||||
const size_t room_ = (ROOM); \
|
||||
while (TypedArrayRoom(A) < room_) { \
|
||||
TypedArrayCapa(A) = TypedArrayCapa(A) < 16 ? 16 : TypedArrayCapa(A) * 2; \
|
||||
/* Largest element count whose byte size still fits a size_t. */ \
|
||||
const size_t maxCapa_ = (size_t) -1 / TypedArrayWidth(A); \
|
||||
const size_t minCapa_ = maxCapa_ < 16 ? maxCapa_ : 16; \
|
||||
size_t capa_ = TypedArrayCapa(A); \
|
||||
assertf(room_ <= maxCapa_ - TypedArraySize(A)); \
|
||||
/* Saturating: a plain capa_*2 wraps to 0 and the loop never ends. */ \
|
||||
while (capa_ - TypedArraySize(A) < room_) { \
|
||||
capa_ = capa_ < minCapa_ ? minCapa_ \
|
||||
: capa_ > maxCapa_ / 2 ? maxCapa_ \
|
||||
: capa_ * 2; \
|
||||
} \
|
||||
TypedArrayPtr(A) = \
|
||||
realloc(TypedArrayPtr(A), TypedArrayCapa(A) * TypedArrayWidth(A)); \
|
||||
TypedArrayCapa(A) = capa_; \
|
||||
TypedArrayPtr(A) = realloct(TypedArrayPtr(A), capa_ * TypedArrayWidth(A)); \
|
||||
if (TypedArrayPtr(A) == NULL) { \
|
||||
hts_record_assert_memory_failed(TypedArrayCapa(A) * TypedArrayWidth(A)); \
|
||||
hts_record_assert_memory_failed(capa_ *TypedArrayWidth(A)); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
@@ -154,8 +162,7 @@ static HTS_UNUSED void hts_record_assert_memory_failed(const size_t size) {
|
||||
do { \
|
||||
if (TypedArrayPtr(A) != NULL) { \
|
||||
TypedArrayCapa(A) = TypedArraySize(A) = 0; \
|
||||
free(TypedArrayPtr(A)); \
|
||||
TypedArrayPtr(A) = NULL; \
|
||||
freet(TypedArrayPtr(A)); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
|
||||
@@ -60,8 +60,6 @@ Please visit our Website: http://www.httrack.com
|
||||
#define HTS_FTRUNCATE(fp, sz) ftruncate(fileno(fp), (sz))
|
||||
#endif
|
||||
|
||||
#define VT_CLREOL "\33[K"
|
||||
|
||||
/* Subdirectory holding a mirrored file's temporaries, beside it. url_savename()
|
||||
maps '~' to '_', so no URL can ever be mirrored inside it (#774, #842). */
|
||||
#define HTS_TMPDIR "~hts-tmp"
|
||||
@@ -174,20 +172,6 @@ int back_connect_fallback_due(int addr_index, int addr_count, int elapsed,
|
||||
return elapsed >= deadline;
|
||||
}
|
||||
|
||||
/* Pending-connect result for a non-blocking socket reported ready by select():
|
||||
0 = connected, >0 = the connect errno (refused, unreachable, ...), -1 if the
|
||||
probe itself failed. A failed connect is reported ready as well (writable on
|
||||
posix, exception set on winsock), so this is how success is told from failure
|
||||
without blocking. */
|
||||
static int connect_socket_error(T_SOC soc) {
|
||||
int soerr = 0;
|
||||
socklen_t len = (socklen_t) sizeof(soerr);
|
||||
|
||||
if (getsockopt(soc, SOL_SOCKET, SO_ERROR, (char *) &soerr, &len) != 0)
|
||||
return -1;
|
||||
return soerr;
|
||||
}
|
||||
|
||||
/* Retry a stuck/failed connecting slot against its next resolved address.
|
||||
Closes the current socket and starts a non-blocking connect to the next
|
||||
candidate, leaving the slot in STATUS_CONNECTING. Returns 1 if a new connect
|
||||
@@ -225,6 +209,9 @@ void back_delete_all(httrackp * opt, cache_back * cache, struct_back * sback) {
|
||||
if (sback != NULL) {
|
||||
int i;
|
||||
|
||||
/* An FTP worker writes through its slot until it returns, so nothing here
|
||||
may wipe or free one under it. */
|
||||
ftp_stop_workers();
|
||||
// delete live slots
|
||||
for(i = 0; i < sback->count; i++) {
|
||||
back_delete(opt, cache, sback, i);
|
||||
@@ -2784,6 +2771,37 @@ void back_clean(httrackp * opt, cache_back * cache, struct_back * sback) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Slot waiting for a connection to come up: nothing requested on it yet. */
|
||||
static hts_boolean back_is_preconnect(const int status) {
|
||||
return status == STATUS_WAIT_DNS || status == STATUS_CONNECTING ||
|
||||
status == STATUS_SSL_WAIT_HANDSHAKE;
|
||||
}
|
||||
|
||||
/* Drop the slots still waiting to connect: nothing to finish there, and left
|
||||
alone they hold the drain for --timeout (#1073). Receiving slots stay. */
|
||||
static void back_stop_preconnect(httrackp *opt, struct_back *sback) {
|
||||
lien_back *const back = sback->lnk;
|
||||
int aborted = 0;
|
||||
int i;
|
||||
|
||||
for (i = 0; i < sback->count; i++) {
|
||||
if (!back_is_preconnect(back[i].status))
|
||||
continue;
|
||||
if (back[i].r.soc != INVALID_SOCKET)
|
||||
deletehttp(&back[i].r);
|
||||
back[i].r.soc = INVALID_SOCKET;
|
||||
/* fatal, as back_add() reports it: a stop must not schedule a retry */
|
||||
back[i].r.statuscode = STATUSCODE_INVALID;
|
||||
strcpybuff(back[i].r.msg, "mirror stopped by user");
|
||||
back_set_finished(sback, i);
|
||||
aborted++;
|
||||
}
|
||||
if (aborted > 0)
|
||||
hts_log_print(opt, LOG_WARNING,
|
||||
"Mirror stopped by user, %d pending connection(s) aborted",
|
||||
aborted);
|
||||
}
|
||||
|
||||
// attente (gestion des buffers des sockets)
|
||||
void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
|
||||
TStamp stat_timestart) {
|
||||
@@ -2813,6 +2831,9 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
|
||||
back_clean(opt, cache, sback);
|
||||
#endif
|
||||
|
||||
if (opt->state.stop)
|
||||
back_stop_preconnect(opt, sback);
|
||||
|
||||
/* Time/size limit exceeded past grace: abort in-flight transfers so no wait
|
||||
loop starves (#481, #77). FTP slots stay, their thread owns the socket. */
|
||||
if (!back_checkmirror(opt)) {
|
||||
@@ -4427,6 +4448,9 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
|
||||
// Content-Length that would overflow the add or the
|
||||
// (size_t) cast is dropped and refetched instead.
|
||||
if (back[i].r.totalsize > INT32_MAX - alloc_mem) {
|
||||
/* Windows refuses to unlink a file still open */
|
||||
fclose(fp);
|
||||
fp = NULL;
|
||||
url_savename_refname_remove(opt, back[i].url_adr,
|
||||
back[i].url_fil);
|
||||
UNLINK(back[i].url_sav);
|
||||
@@ -4457,7 +4481,8 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
|
||||
strcpybuff(back[i].r.msg,
|
||||
"No memory for partial file");
|
||||
}
|
||||
fclose(fp);
|
||||
if (fp != NULL)
|
||||
fclose(fp);
|
||||
} else { // open failed
|
||||
back[i].status = STATUS_READY; // terminé (voir plus loin)
|
||||
back_set_finished(sback, i);
|
||||
@@ -4594,11 +4619,11 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
|
||||
if (opt->verbosedisplay == HTS_VERBOSE_SIMPLE) {
|
||||
if (back[i].status == STATUS_READY) {
|
||||
if (back[i].r.statuscode == HTTP_OK)
|
||||
printf("* %s%s (" LLintP " bytes) - OK" VT_CLREOL "\r",
|
||||
back[i].url_adr, back[i].url_fil, (LLint) back[i].r.size);
|
||||
printf("* %s%s (" LLintP " bytes) - OK\n", back[i].url_adr,
|
||||
back[i].url_fil, (LLint) back[i].r.size);
|
||||
else
|
||||
printf("* %s%s (" LLintP " bytes) - %d" VT_CLREOL "\r",
|
||||
back[i].url_adr, back[i].url_fil, (LLint) back[i].r.size,
|
||||
printf("* %s%s (" LLintP " bytes) - %d\n", back[i].url_adr,
|
||||
back[i].url_fil, (LLint) back[i].r.size,
|
||||
back[i].r.statuscode);
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
@@ -87,8 +87,8 @@ HTS_INLINE int back_exist(struct_back * sback, httrackp * opt, const char *adr,
|
||||
const char *fil, const char *sav);
|
||||
int back_nsoc(const struct_back * sback);
|
||||
int back_nsoc_overall(const struct_back * sback);
|
||||
/* refetch_whole: force a whole-file GET, ignoring any partial/temp-ref resume
|
||||
(set when a prior 206 was rejected as unusable, #581). */
|
||||
/* refetch_whole: force a whole-file GET, ignoring any partial/temp-ref resume;
|
||||
also the caller's latch marking this link's one free restart spent (#581). */
|
||||
int back_add(struct_back *sback, httrackp *opt, cache_back *cache,
|
||||
const char *adr, const char *fil, const char *save,
|
||||
const char *referer_adr, const char *referer_fil, int test,
|
||||
|
||||
@@ -542,6 +542,9 @@ int httpmirror(char *url1, httrackp * opt) {
|
||||
t_cookie BIGSTK cookie; // gestion des cookies
|
||||
|
||||
int ptr; // pointeur actuel sur les liens
|
||||
int retcode = 1; // return code for the single exit; a bailout sets -1
|
||||
hts_boolean rollback = HTS_FALSE; // set to roll back the cache at cleanup
|
||||
hts_boolean completed = HTS_FALSE; // set once the crawl loop reaches its end
|
||||
|
||||
//
|
||||
int numero_passe = 0; // deux passes pour html puis images
|
||||
@@ -939,6 +942,7 @@ int httpmirror(char *url1, httrackp * opt) {
|
||||
fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
|
||||
StringBuff(opt->path_html_utf8), "index.html")),
|
||||
"", "", NULL)) {
|
||||
freet(primary);
|
||||
XH_extuninit; // désallocation mémoire & buffers
|
||||
return 0;
|
||||
}
|
||||
@@ -1114,6 +1118,7 @@ int httpmirror(char *url1, httrackp * opt) {
|
||||
/* Info for wrappers */
|
||||
hts_log_print(opt, LOG_INFO, "engine: start");
|
||||
if (!RUN_CALLBACK0(opt, start)) {
|
||||
freet(primary);
|
||||
XH_extuninit;
|
||||
return 1;
|
||||
}
|
||||
@@ -1278,9 +1283,8 @@ int httpmirror(char *url1, httrackp * opt) {
|
||||
/* Parse */
|
||||
switch (hts_mirror_wait_for_next_file(&str, &stre)) {
|
||||
case -1:
|
||||
XH_uninit;
|
||||
return -1;
|
||||
break;
|
||||
retcode = -1;
|
||||
goto cleanup;
|
||||
case 2:
|
||||
// Jump to 'continue'
|
||||
// This is one of the very very rare cases where goto
|
||||
@@ -1616,8 +1620,8 @@ int httpmirror(char *url1, httrackp * opt) {
|
||||
const int nlinks = opt->lien_tot;
|
||||
|
||||
if (hts_mirror_check_moved(&str, &stre) != 0) {
|
||||
XH_uninit;
|
||||
return -1;
|
||||
retcode = -1;
|
||||
goto cleanup;
|
||||
}
|
||||
/* A redirect re-queues the target as a fresh link; without carrying
|
||||
the marking over, a moved sitemap is fetched and then ignored. */
|
||||
@@ -1805,8 +1809,8 @@ int httpmirror(char *url1, httrackp * opt) {
|
||||
|
||||
/* Parse */
|
||||
if (htsparse(&str, &stre) != 0) {
|
||||
XH_uninit;
|
||||
return -1;
|
||||
retcode = -1;
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2084,10 +2088,11 @@ int httpmirror(char *url1, httrackp * opt) {
|
||||
) {
|
||||
hts_log_print(opt, LOG_NOTICE,
|
||||
"No data seems to have been transferred during this session! : restoring previous one!");
|
||||
XH_uninit;
|
||||
hts_cache_reconcile(opt, CACHE_RECONCILE_ROLLBACK);
|
||||
/* this run replaces nothing, so its archive must not be committed */
|
||||
warc_abort_opt(opt);
|
||||
opt->state.exit_xh = 2; /* interrupted (no connection detected) */
|
||||
return 1;
|
||||
rollback = HTS_TRUE;
|
||||
goto cleanup;
|
||||
}
|
||||
// info text
|
||||
if (cache.txt) {
|
||||
@@ -2287,14 +2292,27 @@ int httpmirror(char *url1, httrackp * opt) {
|
||||
|
||||
// ending
|
||||
usercommand(opt, 0, NULL, NULL, NULL, NULL);
|
||||
warc_close_opt(opt);
|
||||
hts_changes_close_opt(opt);
|
||||
completed = HTS_TRUE;
|
||||
|
||||
cleanup:
|
||||
/* single exit: every bailout jumps here, so the closes below always run */
|
||||
warc_close_opt(opt); /* a no-op once warc_abort_opt() has run */
|
||||
/* An abort never ran hts_changes_indexed(), so its report would be false;
|
||||
free it rather than overwrite the previous run's true one. */
|
||||
if (completed)
|
||||
hts_changes_close_opt(opt);
|
||||
else
|
||||
hts_changes_free_opt(opt);
|
||||
hts_sitemap_free(opt);
|
||||
|
||||
// désallocation mémoire & buffers
|
||||
freet(primary); /* NULL once the first link adopted it */
|
||||
XH_uninit;
|
||||
/* reconcile renames the cache files XH_uninit just closed, so it runs last */
|
||||
if (rollback)
|
||||
hts_cache_reconcile(opt, CACHE_RECONCILE_ROLLBACK);
|
||||
|
||||
return 1; // OK
|
||||
return retcode;
|
||||
}
|
||||
|
||||
// version 2 pour le reste
|
||||
@@ -3393,6 +3411,7 @@ int read_stdin(char *s, int max) {
|
||||
|
||||
#ifdef _WIN32
|
||||
int check_stdin(void) {
|
||||
/* The console queue, not stdin: no end of file to mistake for a keystroke. */
|
||||
return (_kbhit());
|
||||
}
|
||||
#else
|
||||
@@ -3408,11 +3427,18 @@ int check_flot(T_SOC s) {
|
||||
return FD_ISSET(s, &fds);
|
||||
}
|
||||
int check_stdin(void) {
|
||||
int c;
|
||||
|
||||
fflush(stdout);
|
||||
fflush(stdin);
|
||||
if (check_flot(0))
|
||||
return 1;
|
||||
return 0;
|
||||
if (!check_flot(0))
|
||||
return 0;
|
||||
/* Readable is not data: an stdin at EOF stays readable forever and reads as a
|
||||
keypress (#1072). clearerr: a terminal can get input again after a ^D. */
|
||||
clearerr(stdin);
|
||||
if ((c = fgetc(stdin)) == EOF)
|
||||
return 0;
|
||||
ungetc(c, stdin);
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -261,7 +261,7 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
|
||||
#ifdef _WIN32
|
||||
#else
|
||||
/* Terminal is a tty, may ask questions and display funny information */
|
||||
if (isatty(1)) {
|
||||
if (hts_stdout_isterminal()) {
|
||||
opt->quiet = 0;
|
||||
opt->verbosedisplay = HTS_VERBOSE_SIMPLE;
|
||||
}
|
||||
@@ -448,9 +448,10 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
|
||||
|
||||
strcpybuff(tempo, argv[na] + 1);
|
||||
if (hts_lastchar(tempo) != '"') {
|
||||
char BIGSTK s[HTS_CDLMAXSIZE];
|
||||
/* +256 holds the prefix around a max-length argument. */
|
||||
char BIGSTK s[HTS_CDLMAXSIZE + 256];
|
||||
|
||||
sprintf(s, "Missing quote in %s", argv[na]);
|
||||
snprintf(s, sizeof(s), "Missing quote in %s", argv[na]);
|
||||
HTS_PANIC_PRINTF(s);
|
||||
htsmain_free();
|
||||
return -1;
|
||||
@@ -979,7 +980,7 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
|
||||
if (hts_lastchar(tempo) != '"') {
|
||||
char s[HTS_CDLMAXSIZE + 256];
|
||||
|
||||
sprintf(s, "Missing quote in %s", argv[na]);
|
||||
snprintf(s, sizeof(s), "Missing quote in %s", argv[na]);
|
||||
HTS_PANIC_PRINTF(s);
|
||||
htsmain_free();
|
||||
return -1;
|
||||
@@ -1334,6 +1335,16 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
|
||||
opt->mimehtml = 0;
|
||||
com++;
|
||||
}
|
||||
if (opt->mimehtml && opt->single_file) {
|
||||
HTS_PANIC_PRINTF(
|
||||
"-%M and --single-file are two ways to make one "
|
||||
"self-contained file, so pick one: MIME (-%M) carries "
|
||||
"text parts without the base64 tax and stores a shared "
|
||||
"asset once; single-file HTML opens anywhere by "
|
||||
"double-click.");
|
||||
htsmain_free();
|
||||
return -1;
|
||||
}
|
||||
break;
|
||||
case 'k':
|
||||
opt->nokeepalive = 0;
|
||||
@@ -1945,6 +1956,16 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
|
||||
com++;
|
||||
}
|
||||
}
|
||||
if (opt->single_file && opt->mimehtml) {
|
||||
HTS_PANIC_PRINTF(
|
||||
"-%M and --single-file are two ways to make one "
|
||||
"self-contained file, so pick one: MIME (-%M) carries "
|
||||
"text parts without the base64 tax and stores a shared "
|
||||
"asset once; single-file HTML opens anywhere by "
|
||||
"double-click.");
|
||||
htsmain_free();
|
||||
return -1;
|
||||
}
|
||||
break;
|
||||
case 'm': // sitemap / sitemap-url: seed the crawl from sitemaps
|
||||
if (*(com + 1) == 'u') { // --sitemap-url URL: explicit sitemap
|
||||
@@ -2554,20 +2575,21 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
|
||||
else
|
||||
opt->errlog = opt->log;
|
||||
if (opt->log == NULL) {
|
||||
/* path_log is not argv-gated: ~ expansion admits 2047 bytes */
|
||||
char s[HTS_CDLMAXSIZE + 256];
|
||||
|
||||
sprintf(s, "Unable to create log file %s",
|
||||
fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt), StringBuff(opt->path_log),
|
||||
"hts-log.txt"));
|
||||
snprintf(s, sizeof(s), "Unable to create log file %s",
|
||||
fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
|
||||
StringBuff(opt->path_log), "hts-log.txt"));
|
||||
HTS_PANIC_PRINTF(s);
|
||||
htsmain_free();
|
||||
return -1;
|
||||
} else if (opt->errlog == NULL) {
|
||||
char s[HTS_CDLMAXSIZE + 256];
|
||||
|
||||
sprintf(s, "Unable to create log file %s",
|
||||
fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt), StringBuff(opt->path_log),
|
||||
"hts-err.txt"));
|
||||
snprintf(s, sizeof(s), "Unable to create log file %s",
|
||||
fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
|
||||
StringBuff(opt->path_log), "hts-err.txt"));
|
||||
HTS_PANIC_PRINTF(s);
|
||||
htsmain_free();
|
||||
return -1;
|
||||
|
||||
@@ -102,6 +102,13 @@ static mock_host mock_hosts[] = {
|
||||
{"nodns.test", EAI_NONAME, 0, {{0}}, 0},
|
||||
/* resolves, but only well after --timeout: the #606 wedge */
|
||||
{"slow.test", 0, 1, {{AF_INET, {127, 0, 0, 9}}}, 0, MOCK_SLOW_MS},
|
||||
/* a second one, so a cancelled resolve cannot cache an answer the next
|
||||
case then reads instead of asking for */
|
||||
{"slow2.test", 0, 1, {{AF_INET, {127, 0, 0, 10}}}, 0, MOCK_SLOW_MS},
|
||||
/* two more for the cached entry point, checked with the mirror running and
|
||||
then stopped: neither case may read the other's cached answer */
|
||||
{"slow3.test", 0, 1, {{AF_INET, {127, 0, 0, 11}}}, 0, MOCK_SLOW_MS},
|
||||
{"slow4.test", 0, 1, {{AF_INET, {127, 0, 0, 12}}}, 0, MOCK_SLOW_MS},
|
||||
};
|
||||
|
||||
/* Serializes mock_host bookkeeping: a timed-out resolve is abandoned, so its
|
||||
@@ -546,10 +553,57 @@ int dns_timeout_selftests(httrackp *opt) {
|
||||
CHECK(count == 0);
|
||||
CHECK(mock_read_calls("slow.test") == 2); /* re-resolved, not cached */
|
||||
|
||||
/* Both resolves were abandoned mid-backend; wait for their workers to leave
|
||||
it before returning. The backend stays installed: an abandoned worker
|
||||
still reads it (to free its addrinfo) after the last call returns. */
|
||||
mock_wait_finished(2);
|
||||
/* A raised cancel flag ends the wait where no deadline would, which is what
|
||||
lets a stopped FTP worker leave a black-holed resolver (#1059). */
|
||||
{
|
||||
hts_boolean cancel = HTS_TRUE;
|
||||
|
||||
start = mtime_local();
|
||||
count = hts_dns_resolve_all_bounded(opt, "slow.test", addrs, HTS_MAXADDRNUM,
|
||||
0, &cancel, &err);
|
||||
elapsed = mtime_local() - start;
|
||||
CHECK(count == 0);
|
||||
CHECK(elapsed < MOCK_SLOW_MS / 2);
|
||||
}
|
||||
|
||||
/* And an unbounded resolve nobody cancels still waits for its answer: a
|
||||
timeout of 0 must not be read back as a deadline of "now". */
|
||||
{
|
||||
hts_boolean cancel = HTS_FALSE;
|
||||
|
||||
start = mtime_local();
|
||||
count = hts_dns_resolve_all_bounded(opt, "slow2.test", addrs,
|
||||
HTS_MAXADDRNUM, 0, &cancel, &err);
|
||||
elapsed = mtime_local() - start;
|
||||
CHECK(count == 1);
|
||||
CHECK(elapsed >= MOCK_SLOW_MS / 2);
|
||||
}
|
||||
|
||||
/* The cached entry point takes the mirror's own stop flag as its cancel, so
|
||||
a ^C ends a resolve nothing else would bound (#1073). Unbounded here, to
|
||||
tell the stop apart from a deadline. */
|
||||
opt->timeout = 0;
|
||||
{
|
||||
/* control first: with the mirror running, the same call still waits */
|
||||
start = mtime_local();
|
||||
count = hts_dns_resolve_all(opt, "slow3.test", addrs, HTS_MAXADDRNUM, &err);
|
||||
elapsed = mtime_local() - start;
|
||||
CHECK(count == 1);
|
||||
CHECK(elapsed >= MOCK_SLOW_MS / 2);
|
||||
|
||||
opt->state.stop = 1; /* the flag, not hts_request_stop's log line */
|
||||
start = mtime_local();
|
||||
count = hts_dns_resolve_all(opt, "slow4.test", addrs, HTS_MAXADDRNUM, &err);
|
||||
elapsed = mtime_local() - start;
|
||||
opt->state.stop = 0;
|
||||
CHECK(count == 0);
|
||||
CHECK(elapsed < MOCK_SLOW_MS / 2);
|
||||
}
|
||||
|
||||
/* Four of the six resolves were abandoned mid-backend; wait for their
|
||||
workers to leave it before returning. The backend stays installed: an
|
||||
abandoned worker still reads it (to free its addrinfo). */
|
||||
mock_wait_finished(6);
|
||||
return failures;
|
||||
}
|
||||
|
||||
|
||||
366
src/htsftp.c
366
src/htsftp.c
@@ -40,6 +40,9 @@ Please visit our Website: http://www.httrack.com
|
||||
|
||||
#include "htscore.h"
|
||||
#include "htsthread.h"
|
||||
|
||||
#include <limits.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#else
|
||||
//inet_ntoa
|
||||
@@ -60,12 +63,55 @@ Please visit our Website: http://www.httrack.com
|
||||
|
||||
#if USE_BEGINTHREAD
|
||||
|
||||
/* Live FTP workers. Each writes through its backlog slot and reads opt for its
|
||||
whole run, so neither may be freed while the list is not empty (#1051).
|
||||
Registration happens at spawn, not at thread entry: a worker that has not
|
||||
been scheduled yet must already hold the list against a teardown. */
|
||||
static FTPDownloadStruct *ftp_workers = NULL;
|
||||
static htsmutex ftp_workers_mutex = HTSMUTEX_INIT;
|
||||
|
||||
static void ftp_worker_register(FTPDownloadStruct *worker) {
|
||||
hts_mutexlock(&ftp_workers_mutex);
|
||||
worker->pNext = ftp_workers;
|
||||
ftp_workers = worker;
|
||||
hts_mutexrelease(&ftp_workers_mutex);
|
||||
}
|
||||
|
||||
static void ftp_worker_unregister(FTPDownloadStruct *worker) {
|
||||
FTPDownloadStruct **prev;
|
||||
|
||||
hts_mutexlock(&ftp_workers_mutex);
|
||||
for (prev = &ftp_workers; *prev != NULL; prev = &(*prev)->pNext) {
|
||||
if (*prev == worker) {
|
||||
*prev = worker->pNext;
|
||||
break;
|
||||
}
|
||||
}
|
||||
hts_mutexrelease(&ftp_workers_mutex);
|
||||
}
|
||||
|
||||
void ftp_stop_workers(void) {
|
||||
int wait;
|
||||
|
||||
do {
|
||||
FTPDownloadStruct *worker;
|
||||
|
||||
hts_mutexlock(&ftp_workers_mutex);
|
||||
/* Idempotent, and re-raised each pass so a worker registered meanwhile is
|
||||
not missed. A registered worker's slot is still live by construction:
|
||||
back_delete_all() drains before back_free(). */
|
||||
for (worker = ftp_workers; worker != NULL; worker = worker->pNext)
|
||||
worker->pBack->stop_ftp = HTS_TRUE;
|
||||
wait = ftp_workers != NULL;
|
||||
hts_mutexrelease(&ftp_workers_mutex);
|
||||
if (wait)
|
||||
Sleep(100);
|
||||
} while (wait);
|
||||
}
|
||||
|
||||
void back_launch_ftp(void *pP) {
|
||||
FTPDownloadStruct *pStruct = (FTPDownloadStruct *) pP;
|
||||
|
||||
if (pStruct == NULL)
|
||||
return;
|
||||
|
||||
if (pStruct == NULL) {
|
||||
#if FTP_DEBUG
|
||||
printf("[ftp error: no args]\n");
|
||||
@@ -84,11 +130,15 @@ void back_launch_ftp(void *pP) {
|
||||
// prêt
|
||||
pStruct->pBack->status = STATUS_FTP_READY;
|
||||
|
||||
/* Delete structure */
|
||||
free(pP);
|
||||
|
||||
/* Uninitialize */
|
||||
hts_uninit();
|
||||
|
||||
/* The status store above was this worker's last read of the slot and of opt;
|
||||
deregistering lets the engine free both. */
|
||||
ftp_worker_unregister(pStruct);
|
||||
|
||||
/* Delete structure */
|
||||
free(pP);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -98,7 +148,16 @@ void launch_ftp(FTPDownloadStruct * params) {
|
||||
#if FTP_DEBUG
|
||||
printf("[Launching main ftp thread]\n");
|
||||
#endif
|
||||
hts_newthread(back_launch_ftp, (void *) params);
|
||||
ftp_worker_register(params);
|
||||
if (hts_newthread(back_launch_ftp, (void *) params) != 0) {
|
||||
/* Nobody would ever reap a slot left in STATUS_FTP_TRANSFER, and
|
||||
ftp_stop_workers() would never return. */
|
||||
ftp_worker_unregister(params);
|
||||
strcpybuff(params->pBack->r.msg, "Unable to launch FTP thread");
|
||||
params->pBack->r.statuscode = STATUSCODE_INVALID;
|
||||
params->pBack->status = STATUS_FTP_READY;
|
||||
free(params);
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
@@ -122,42 +181,48 @@ void launch_ftp(FTPDownloadStruct * params) {
|
||||
return 0; \
|
||||
}
|
||||
|
||||
/* Bounded split of a hostile-URL "user[:pass]@" prefix (see htsftp.h). */
|
||||
void ftp_split_userpass(const char *src, const char *end, char *user,
|
||||
size_t user_size, char *pass, size_t pass_size) {
|
||||
size_t n = 0;
|
||||
/* Split a hostile-URL "user[:pass]@" prefix (see htsftp.h). */
|
||||
hts_boolean ftp_split_userpass(const char *src, const char *end, char *user,
|
||||
size_t user_size, char *pass, size_t pass_size) {
|
||||
size_t len = 0, user_len, pass_len;
|
||||
const char *colon;
|
||||
|
||||
assertf(user_size > 0 && pass_size > 0); /* the size-1 math underflows on 0 */
|
||||
assertf(end > src); /* end is one past the '@' */
|
||||
|
||||
while (src[n] != '\0' && src[n] != ':') {
|
||||
if (n < user_size - 1)
|
||||
user[n] = src[n];
|
||||
n++;
|
||||
}
|
||||
user[n < user_size ? n : user_size - 1] = '\0';
|
||||
pass[0] = '\0';
|
||||
if (src[n] == ':') { // password follows the colon
|
||||
const size_t base = n + 1;
|
||||
size_t k = 0;
|
||||
|
||||
while (&src[base + k + 1] < end && src[base + k] != '\0') {
|
||||
if (k < pass_size - 1)
|
||||
pass[k] = src[base + k];
|
||||
k++;
|
||||
}
|
||||
pass[k < pass_size ? k : pass_size - 1] = '\0';
|
||||
}
|
||||
user[0] = pass[0] = '\0'; // fail safe for a caller that ignores the result
|
||||
while (len < (size_t) (end - src) - 1 && src[len] != '\0')
|
||||
len++;
|
||||
colon = memchr(src, ':', len);
|
||||
user_len = colon != NULL ? (size_t) (colon - src) : len;
|
||||
pass_len = colon != NULL ? len - user_len - 1 : 0;
|
||||
/* a clipped name is another account's, so refuse rather than log in as it */
|
||||
if (user_len >= user_size || pass_len >= pass_size)
|
||||
return HTS_FALSE;
|
||||
memcpy(user, src, user_len);
|
||||
user[user_len] = '\0';
|
||||
if (pass_len != 0)
|
||||
memcpy(pass, colon + 1, pass_len);
|
||||
pass[pass_len] = '\0';
|
||||
return HTS_TRUE;
|
||||
}
|
||||
|
||||
/* Build "<verb> <path>" (see htsftp.h). */
|
||||
void ftp_command(char *line, size_t line_size, const char *verb,
|
||||
const char *path) {
|
||||
hts_boolean ftp_command(char *line, size_t line_size, const char *verb,
|
||||
const char *path) {
|
||||
int n;
|
||||
|
||||
/* A leading '-' would reach a server that shells out to ls as a flag. */
|
||||
if (path[0] == '-' || strchr(path, ' ') != NULL ||
|
||||
strchr(path, '\"') != NULL || strchr(path, '\'') != NULL)
|
||||
snprintf(line, line_size, "%s \"%s\"", verb, path);
|
||||
n = snprintf(line, line_size, "%s \"%s\"", verb, path);
|
||||
else
|
||||
snprintf(line, line_size, "%s %s", verb, path);
|
||||
n = snprintf(line, line_size, "%s %s", verb, path);
|
||||
if (n < 0 || (size_t) n >= line_size) {
|
||||
line[0] = '\0'; // fail safe for a caller that ignores the result
|
||||
return HTS_FALSE;
|
||||
}
|
||||
return HTS_TRUE;
|
||||
}
|
||||
|
||||
/* MDTM reply "213 YYYYMMDDHHMMSS[.frac]" (RFC 3659, UTC) into tm_time. */
|
||||
@@ -205,13 +270,76 @@ static hts_boolean ftp_may_resume(httrackp *opt, const lien_back *back,
|
||||
return HTS_TRUE;
|
||||
}
|
||||
|
||||
/* Clip a wait to what is left of --max-time: back_wait() can't abort this slot
|
||||
the way it aborts an HTTP one. */
|
||||
static int ftp_wait_left(const httrackp *opt, int timeout) {
|
||||
if (opt != NULL && opt->maxtime > 0) {
|
||||
const TStamp left =
|
||||
(TStamp) opt->maxtime - (time_local() - HTS_STAT.stat_timestart);
|
||||
|
||||
if (left < (TStamp) timeout)
|
||||
timeout = left > 0 ? (int) left : 0;
|
||||
}
|
||||
return timeout;
|
||||
}
|
||||
|
||||
/* Connect honoring the stop flag, --timeout and --max-time: a blocking
|
||||
connect() to a black hole sits for the kernel's own two minutes (#1059). */
|
||||
static hts_boolean ftp_connect(lien_back *back, T_SOC soc, SOCaddr *server,
|
||||
int timeout, const httrackp *opt) {
|
||||
const TStamp started = time_local();
|
||||
|
||||
timeout = ftp_wait_left(opt, timeout);
|
||||
if (!socket_set_nonblocking(soc, HTS_TRUE))
|
||||
return HTS_FALSE;
|
||||
if (connect(soc, &SOCaddr_sockaddr(*server), SOCaddr_size(*server)) != 0) {
|
||||
#ifdef _WIN32
|
||||
if (WSAGetLastError() != WSAEWOULDBLOCK)
|
||||
#else
|
||||
if (errno != EINPROGRESS && errno != EINTR)
|
||||
#endif
|
||||
return HTS_FALSE;
|
||||
/* state.stop counts here, unlike in the waits below: a mirror the user
|
||||
stopped has nothing to finish on a connection never established. */
|
||||
while (check_socket_connect(soc) == 0) {
|
||||
if (back->stop_ftp || (opt != NULL && opt->state.stop) ||
|
||||
(int) (time_local() - started) >= timeout)
|
||||
return HTS_FALSE;
|
||||
Sleep(100);
|
||||
}
|
||||
if (connect_socket_error(soc) != 0)
|
||||
return HTS_FALSE;
|
||||
}
|
||||
/* the rest of the session reads and writes this socket blocking */
|
||||
return socket_set_nonblocking(soc, HTS_FALSE);
|
||||
}
|
||||
|
||||
/* Resolve, likewise: the shared resolver bounds itself by opt->timeout alone
|
||||
and takes no stop flag. */
|
||||
static SOCaddr *ftp_dns_resolve(httrackp *opt, lien_back *back,
|
||||
const char *host, SOCaddr *addr, int timeout,
|
||||
const char **error) {
|
||||
const int left = ftp_wait_left(opt, timeout);
|
||||
|
||||
SOCaddr_clear(*addr);
|
||||
if (left <= 0) {
|
||||
if (error != NULL)
|
||||
*error = "mirror deadline reached";
|
||||
return NULL;
|
||||
}
|
||||
if (hts_dns_resolve_all_bounded(opt, host, addr, 1, left, &back->stop_ftp,
|
||||
error) > 0)
|
||||
return SOCaddr_is_valid(*addr) ? addr : NULL;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// la véritable fonction une fois lancées les routines thread/fork
|
||||
int run_launch_ftp(FTPDownloadStruct * pStruct) {
|
||||
lien_back *back = pStruct->pBack;
|
||||
httrackp *opt = pStruct->pOpt;
|
||||
char user[256] = "anonymous";
|
||||
char pass[256] = "user@";
|
||||
char line_retr[2048];
|
||||
char line_retr[FTP_LINE_SIZE];
|
||||
int port = 21;
|
||||
|
||||
#if FTP_PASV
|
||||
@@ -220,7 +348,9 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
|
||||
char BIGSTK adr_ip[1024];
|
||||
char *adr, *real_adr;
|
||||
char BIGSTK ftp_path[CATBUFF_SIZE]; // the decoded URL path, screened once
|
||||
int timeout = 300; // timeout
|
||||
/* opt->timeout == 0 means unlimited; use INT_MAX so 0 doesn't make the read
|
||||
return instantly. */
|
||||
const int timeout = opt->timeout > 0 ? opt->timeout : INT_MAX;
|
||||
int timeout_onfly = 8; // attente réponse supplémentaire
|
||||
int transfer_list = 0; // directory
|
||||
int rest_understood = 0; // rest command understood
|
||||
@@ -234,8 +364,6 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
|
||||
//
|
||||
line_retr[0] = adr_ip[0] = ftp_path[0] = '\0';
|
||||
|
||||
timeout = 300;
|
||||
|
||||
// effacer
|
||||
strcpybuff(back->r.msg, "");
|
||||
back->r.statuscode = 0;
|
||||
@@ -251,7 +379,11 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
|
||||
while(*real_adr == '/')
|
||||
real_adr++; // sauter /
|
||||
if ((adr = jump_identification(real_adr)) != real_adr) { // user
|
||||
ftp_split_userpass(real_adr, adr, user, sizeof(user), pass, sizeof(pass));
|
||||
if (!ftp_split_userpass_buf(real_adr, adr, user, pass)) {
|
||||
strcpybuff(back->r.msg, "FTP user name or password too long");
|
||||
back->r.statuscode = STATUSCODE_INVALID;
|
||||
_HALT_FTP return 0;
|
||||
}
|
||||
}
|
||||
// Calculer RETR <nom>
|
||||
{
|
||||
@@ -262,13 +394,19 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
|
||||
if (strnotempty(a)) {
|
||||
const size_t len_a =
|
||||
strlen(unescape_http(ftp_path, sizeof(ftp_path), a));
|
||||
hts_boolean fits;
|
||||
|
||||
if (len_a > 0 &&
|
||||
ftp_path[len_a - 1] == '/') { /* obviously a directory listing */
|
||||
transfer_list = 1;
|
||||
ftp_command(line_retr, sizeof(line_retr), "LIST -A", ftp_path);
|
||||
fits = ftp_command_line(line_retr, "LIST -A", ftp_path);
|
||||
} else {
|
||||
ftp_command(line_retr, sizeof(line_retr), "RETR", ftp_path);
|
||||
fits = ftp_command_line(line_retr, "RETR", ftp_path);
|
||||
}
|
||||
if (!fits) {
|
||||
strcpybuff(back->r.msg, "FTP path too long");
|
||||
back->r.statuscode = STATUSCODE_INVALID;
|
||||
_HALT_FTP return 0;
|
||||
}
|
||||
} else {
|
||||
transfer_list = 1;
|
||||
@@ -298,6 +436,7 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
|
||||
SOCaddr server;
|
||||
char *a;
|
||||
char _adr[256];
|
||||
size_t adr_len;
|
||||
const char *error = "unknown error";
|
||||
|
||||
_adr[0] = '\0';
|
||||
@@ -315,13 +454,20 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
|
||||
back->r.statuscode = STATUSCODE_INVALID; // permanent, unlike a DNS miss
|
||||
_HALT_FTP return 0;
|
||||
}
|
||||
strncatbuff(_adr, adr, (int) (a - adr));
|
||||
adr_len = (size_t) (a - adr);
|
||||
} else
|
||||
strcpybuff(_adr, adr);
|
||||
adr_len = strlen(adr);
|
||||
// no resolvable name is this long, and clipping would query another host
|
||||
if (adr_len >= sizeof(_adr)) {
|
||||
htsblk_failf(&back->r, "Host name too long");
|
||||
back->r.statuscode = STATUSCODE_INVALID;
|
||||
_HALT_FTP return 0;
|
||||
}
|
||||
strncatbuff(_adr, adr, (int) adr_len);
|
||||
|
||||
// récupérer adresse résolue
|
||||
strcpybuff(back->info, "host name");
|
||||
if (hts_dns_resolve2(opt, _adr, &server, &error) == NULL) {
|
||||
if (ftp_dns_resolve(opt, back, _adr, &server, timeout, &error) == NULL) {
|
||||
htsblk_failf(&back->r, "Unable to get server's address: %s", error);
|
||||
back->r.statuscode = STATUSCODE_NON_FATAL;
|
||||
_HALT_FTP return 0;
|
||||
@@ -341,34 +487,33 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
|
||||
|
||||
SOCaddr_initport(server, port);
|
||||
|
||||
// connexion (bloquante, on est en thread)
|
||||
strcpybuff(back->info, "connect");
|
||||
hts_log_print(opt, LOG_DEBUG, "FTP: connecting to %s:%d", _adr, port);
|
||||
|
||||
if (connect(soc_ctl, &SOCaddr_sockaddr(server), SOCaddr_size(server)) != 0) {
|
||||
if (!ftp_connect(back, soc_ctl, &server, timeout, opt)) {
|
||||
strcpybuff(back->r.msg, "Unable to connect to the server");
|
||||
back->r.statuscode = STATUSCODE_INVALID;
|
||||
_HALT_FTP return 0;
|
||||
#ifdef _WIN32
|
||||
}
|
||||
#else
|
||||
}
|
||||
#endif
|
||||
_CHECK_HALT_FTP;
|
||||
|
||||
{
|
||||
char BIGSTK line[1024];
|
||||
char BIGSTK line[FTP_LINE_SIZE];
|
||||
|
||||
/* line_retr is copied here verbatim; a narrower line[] would clip it. */
|
||||
HTS_COMPILE_ASSERT(sizeof(line) == sizeof(line_retr));
|
||||
|
||||
// envoi du login
|
||||
|
||||
// --USER--
|
||||
get_ftp_line(soc_ctl, line, sizeof(line), timeout); // en tête
|
||||
get_ftp_line(back, soc_ctl, line, sizeof(line), timeout, opt); // en tête
|
||||
_CHECK_HALT_FTP;
|
||||
|
||||
if (line[0] == '2') { // ok, connecté
|
||||
strcpybuff(back->info, "login: user");
|
||||
snprintf(line, sizeof(line), "USER %s", user);
|
||||
send_line(soc_ctl, line);
|
||||
get_ftp_line(soc_ctl, line, sizeof(line), timeout);
|
||||
get_ftp_line(back, soc_ctl, line, sizeof(line), timeout, opt);
|
||||
_CHECK_HALT_FTP;
|
||||
if ((line[0] == '3') || (line[0] == '2')) {
|
||||
// --PASS--
|
||||
@@ -376,12 +521,12 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
|
||||
strcpybuff(back->info, "login: pass");
|
||||
snprintf(line, sizeof(line), "PASS %s", pass);
|
||||
send_line(soc_ctl, line);
|
||||
get_ftp_line(soc_ctl, line, sizeof(line), timeout);
|
||||
get_ftp_line(back, soc_ctl, line, sizeof(line), timeout, opt);
|
||||
_CHECK_HALT_FTP;
|
||||
}
|
||||
if (line[0] == '2') { // ok
|
||||
send_line(soc_ctl, "TYPE I");
|
||||
get_ftp_line(soc_ctl, line, sizeof(line), timeout);
|
||||
get_ftp_line(back, soc_ctl, line, sizeof(line), timeout, opt);
|
||||
_CHECK_HALT_FTP;
|
||||
if (line[0] == '2') {
|
||||
// ok
|
||||
@@ -414,7 +559,7 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
|
||||
strcpybuff(back->info, "pasv");
|
||||
snprintf(line, sizeof(line), "PASV");
|
||||
send_line(soc_ctl, line);
|
||||
get_ftp_line(soc_ctl, line, sizeof(line), timeout);
|
||||
get_ftp_line(back, soc_ctl, line, sizeof(line), timeout, opt);
|
||||
} else { /* ipv6 */
|
||||
line[0] = '\0';
|
||||
}
|
||||
@@ -476,7 +621,7 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
|
||||
strcpybuff(back->info, "pasv");
|
||||
snprintf(line, sizeof(line), "EPSV");
|
||||
send_line(soc_ctl, line);
|
||||
get_ftp_line(soc_ctl, line, sizeof(line), timeout);
|
||||
get_ftp_line(back, soc_ctl, line, sizeof(line), timeout, opt);
|
||||
_CHECK_HALT_FTP;
|
||||
if (line[0] == '2') { /* got it */
|
||||
char *a;
|
||||
@@ -514,13 +659,12 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
|
||||
#endif
|
||||
// SIZE
|
||||
if (back->r.statuscode != -1) {
|
||||
if (!transfer_list) {
|
||||
ftp_command(line, sizeof(line), "SIZE", ftp_path);
|
||||
|
||||
// a clipped probe would size and date a different file
|
||||
if (!transfer_list && ftp_command_line(line, "SIZE", ftp_path)) {
|
||||
// SIZE?
|
||||
strcpybuff(back->info, "size");
|
||||
send_line(soc_ctl, line);
|
||||
get_ftp_line(soc_ctl, line, sizeof(line), timeout);
|
||||
get_ftp_line(back, soc_ctl, line, sizeof(line), timeout, opt);
|
||||
_CHECK_HALT_FTP;
|
||||
if (line[0] == '2') { // SIZE compris, ALORS tester REST (sinon pas tester: cf probleme des txt.gz decompresses a la volee)
|
||||
char *szstr = strchr(line, ' ');
|
||||
@@ -537,22 +681,24 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
|
||||
}
|
||||
|
||||
// MDTM?
|
||||
ftp_command(line, sizeof(line), "MDTM", ftp_path);
|
||||
strcpybuff(back->info, "mdtm");
|
||||
send_line(soc_ctl, line);
|
||||
get_ftp_line(soc_ctl, line, sizeof(line), timeout);
|
||||
_CHECK_HALT_FTP;
|
||||
if (ftp_parse_mdtm(line, &remote_tm)) {
|
||||
char date[256];
|
||||
if (ftp_command_line(line, "MDTM", ftp_path)) {
|
||||
strcpybuff(back->info, "mdtm");
|
||||
send_line(soc_ctl, line);
|
||||
get_ftp_line(back, soc_ctl, line, sizeof(line), timeout, opt);
|
||||
_CHECK_HALT_FTP;
|
||||
if (ftp_parse_mdtm(line, &remote_tm)) {
|
||||
char date[256];
|
||||
|
||||
time_rfc822(date, &remote_tm);
|
||||
/* Stamp the mirror as the HTTP path does, so a later pass
|
||||
compares server-clock times instead of crossing clocks. */
|
||||
back->r.lastmodified[0] = '\0';
|
||||
strlncatbuff(back->r.lastmodified, date,
|
||||
sizeof(back->r.lastmodified),
|
||||
sizeof(back->r.lastmodified) - 1);
|
||||
remote_mtime = timegm(&remote_tm);
|
||||
time_rfc822(date, &remote_tm);
|
||||
/* Stamp the mirror as the HTTP path does, so a later pass
|
||||
compares server-clock times instead of crossing clocks.
|
||||
*/
|
||||
back->r.lastmodified[0] = '\0';
|
||||
strlncatbuff(back->r.lastmodified, date,
|
||||
sizeof(back->r.lastmodified),
|
||||
sizeof(back->r.lastmodified) - 1);
|
||||
remote_mtime = timegm(&remote_tm);
|
||||
}
|
||||
}
|
||||
|
||||
/* Only over a copy back_add() judged partial: on --update every
|
||||
@@ -564,13 +710,13 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
|
||||
snprintf(line, sizeof(line), "REST " LLintP,
|
||||
(LLint) back->range_req_size);
|
||||
send_line(soc_ctl, line);
|
||||
get_ftp_line(soc_ctl, line, sizeof(line), timeout);
|
||||
get_ftp_line(back, soc_ctl, line, sizeof(line), timeout, opt);
|
||||
_CHECK_HALT_FTP;
|
||||
if ((line[0] == '3') || (line[0] == '2')) { // ok
|
||||
rest_understood = 1;
|
||||
} // else never mind
|
||||
}
|
||||
} // sinon tant pis
|
||||
} // sinon tant pis
|
||||
}
|
||||
}
|
||||
#if FTP_PASV
|
||||
@@ -584,7 +730,7 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
|
||||
// Ok, se connecter
|
||||
if (port_pasv) {
|
||||
SOCaddr server;
|
||||
int server_size = sizeof(server);
|
||||
hts_boolean resolved;
|
||||
const char *error = "unknown error";
|
||||
|
||||
// effacer structure
|
||||
@@ -595,9 +741,11 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
|
||||
|
||||
// résoudre
|
||||
if (adr_ip[0]) {
|
||||
hts_dns_resolve2(opt, adr_ip, &server, &error);
|
||||
resolved = ftp_dns_resolve(opt, back, adr_ip, &server, timeout,
|
||||
&error) != NULL;
|
||||
} else {
|
||||
SOCaddr_copy_SOCaddr(server, server_data);
|
||||
resolved = HTS_TRUE;
|
||||
}
|
||||
|
||||
// infos
|
||||
@@ -605,17 +753,19 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
|
||||
#if FTP_DEBUG
|
||||
printf("Data: Connecting to %s:%d...\n", adr_ip, port_pasv);
|
||||
#endif
|
||||
if (server_size > 0) {
|
||||
if (resolved) {
|
||||
// socket
|
||||
soc_dat = (T_SOC) socket(SOCaddr_sinfamily(server), SOCK_STREAM, 0);
|
||||
if (soc_dat != INVALID_SOCKET) {
|
||||
// structure: connexion au domaine internet, port 80 (ou autre)
|
||||
SOCaddr_initport(server, port_pasv);
|
||||
if (connect(soc_dat, &SOCaddr_sockaddr(server), SOCaddr_size(server)) == 0) {
|
||||
if (ftp_connect(back, soc_dat, &server, timeout, opt)) {
|
||||
strcpybuff(back->info, "retr");
|
||||
strcpybuff(line, line_retr);
|
||||
// clip, never abort: this line is built from a crawled URL
|
||||
line[0] = '\0';
|
||||
strlncatbuff(line, line_retr, sizeof(line), sizeof(line) - 1);
|
||||
send_line(soc_ctl, line);
|
||||
get_ftp_line(soc_ctl, line, sizeof(line), timeout);
|
||||
get_ftp_line(back, soc_ctl, line, sizeof(line), timeout, opt);
|
||||
_CHECK_HALT_FTP;
|
||||
if (line[0] == '1') {
|
||||
// OK
|
||||
@@ -636,7 +786,7 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
|
||||
//
|
||||
strcpybuff(back->r.msg, "Unable to connect");
|
||||
back->r.statuscode = STATUSCODE_INVALID;
|
||||
} // sinon on est prêts
|
||||
} // sinon on est prêts
|
||||
} else {
|
||||
strcpybuff(back->r.msg, "Unable to create a socket");
|
||||
back->r.statuscode = STATUSCODE_INVALID;
|
||||
@@ -645,7 +795,7 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
|
||||
htsblk_failf(&back->r, "Unable to resolve IP %s: %s", adr_ip,
|
||||
error);
|
||||
back->r.statuscode = STATUSCODE_INVALID;
|
||||
} // sinon on est prêts
|
||||
} // sinon on est prêts
|
||||
} else {
|
||||
htsblk_failf(&back->r, "PASV incorrect: %s", linejmp(line));
|
||||
back->r.statuscode = STATUSCODE_INVALID;
|
||||
@@ -656,13 +806,15 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
|
||||
if ((soc_servdat = get_datasocket(line, sizeof(line))) != INVALID_SOCKET) {
|
||||
_CHECK_HALT_FTP;
|
||||
send_line(soc_ctl, line); // envoi du RETR
|
||||
get_ftp_line(soc_ctl, line, sizeof(line), timeout);
|
||||
get_ftp_line(back, soc_ctl, line, sizeof(line), timeout, opt);
|
||||
_CHECK_HALT_FTP;
|
||||
if (line[0] == '2') { // ok
|
||||
strcpybuff(back->info, "retr");
|
||||
strcpybuff(line, line_retr);
|
||||
// clip, never abort: this line is built from a crawled URL
|
||||
line[0] = '\0';
|
||||
strlncatbuff(line, line_retr, sizeof(line), sizeof(line) - 1);
|
||||
send_line(soc_ctl, line);
|
||||
get_ftp_line(soc_ctl, line, sizeof(line), timeout);
|
||||
get_ftp_line(back, soc_ctl, line, sizeof(line), timeout, opt);
|
||||
_CHECK_HALT_FTP;
|
||||
if (line[0] == '1') {
|
||||
//T_SOC soc_dat;
|
||||
@@ -717,14 +869,17 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
|
||||
while((len > 0) && (!stop_ftp(back))) {
|
||||
// attendre les données
|
||||
len = 1; // pas d'erreur pour le moment
|
||||
switch (wait_socket_receive(soc_dat, timeout)) {
|
||||
switch (wait_socket_receive(back, soc_dat, timeout, opt)) {
|
||||
case -1:
|
||||
strcpybuff(back->r.msg, "FTP read error");
|
||||
back->r.statuscode = STATUSCODE_INVALID;
|
||||
len = 0; // fin
|
||||
break;
|
||||
case 0:
|
||||
htsblk_failf(&back->r, "Time out (%d)", timeout);
|
||||
/* stop_ftp() names the real reason when it is the stop that
|
||||
cut the wait short. */
|
||||
if (!stop_ftp(back))
|
||||
htsblk_failf(&back->r, "Time out (%d)", timeout);
|
||||
back->r.statuscode = STATUSCODE_INVALID;
|
||||
len = 0; // fin
|
||||
break;
|
||||
@@ -784,9 +939,9 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
|
||||
|
||||
// 226 Transfer complete?
|
||||
if (back->r.statuscode != -1) {
|
||||
if (wait_socket_receive(soc_ctl, timeout_onfly) > 0) {
|
||||
if (wait_socket_receive(back, soc_ctl, timeout_onfly, opt) > 0) {
|
||||
// récupérer 226 transfer complete
|
||||
get_ftp_line(soc_ctl, line, sizeof(line), timeout);
|
||||
get_ftp_line(back, soc_ctl, line, sizeof(line), timeout, opt);
|
||||
if (line[0] == '2') { // OK
|
||||
strcpybuff(back->r.msg, "OK");
|
||||
back->r.statuscode = HTTP_OK;
|
||||
@@ -809,7 +964,7 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
|
||||
_CHECK_HALT_FTP;
|
||||
strcpybuff(back->info, "quit");
|
||||
send_line(soc_ctl, "QUIT"); // bye bye
|
||||
get_ftp_line(soc_ctl, NULL, 0, timeout);
|
||||
get_ftp_line(back, soc_ctl, NULL, 0, timeout, opt);
|
||||
#ifdef _WIN32
|
||||
closesocket(soc_ctl);
|
||||
#else
|
||||
@@ -922,7 +1077,8 @@ FILE *dd = NULL;
|
||||
// routines de réception/émission
|
||||
// 0 = ERROR
|
||||
int send_line(T_SOC soc, const char *data) {
|
||||
char BIGSTK line[1024];
|
||||
char BIGSTK line[FTP_LINE_SIZE + 2]; // room for the CRLF of a maximal command
|
||||
int n;
|
||||
|
||||
// backstop: the driver fails earlier, but no injected byte reaches the wire
|
||||
if (!hts_is_control_free(data))
|
||||
@@ -942,7 +1098,10 @@ int send_line(T_SOC soc, const char *data) {
|
||||
printf("---> %s", data);
|
||||
fflush(stdout);
|
||||
#endif
|
||||
snprintf(line, sizeof(line), "%s\x0d\x0a", data);
|
||||
// an unterminated command would blend into whatever the server reads next
|
||||
n = snprintf(line, sizeof(line), "%s\x0d\x0a", data);
|
||||
if (n < 0 || n >= (int) sizeof(line))
|
||||
return 0;
|
||||
if (check_socket_connect(soc) != 1) {
|
||||
#if FTP_DEBUG
|
||||
printf("!SOC WRITE ERROR\n");
|
||||
@@ -962,7 +1121,8 @@ int send_line(T_SOC soc, const char *data) {
|
||||
#endif
|
||||
}
|
||||
|
||||
int get_ftp_line(T_SOC soc, char *ptrline, size_t line_size, int timeout) {
|
||||
int get_ftp_line(lien_back *back, T_SOC soc, char *ptrline, size_t line_size,
|
||||
int timeout, const httrackp *opt) {
|
||||
char BIGSTK data[1024];
|
||||
int i, ok, multiline;
|
||||
|
||||
@@ -978,7 +1138,7 @@ int get_ftp_line(T_SOC soc, char *ptrline, size_t line_size, int timeout) {
|
||||
char b;
|
||||
|
||||
// vérifier données
|
||||
switch (wait_socket_receive(soc, timeout)) {
|
||||
switch (wait_socket_receive(back, soc, timeout, opt)) {
|
||||
case -1: // erreur de lecture
|
||||
if (ptrline)
|
||||
snprintf(ptrline, line_size, "500 *read error");
|
||||
@@ -1090,17 +1250,21 @@ int check_socket_connect(T_SOC soc) {
|
||||
}
|
||||
|
||||
// attendre des données
|
||||
int wait_socket_receive(T_SOC soc, int timeout) {
|
||||
// attendre les données
|
||||
int wait_socket_receive(lien_back *back, T_SOC soc, int timeout,
|
||||
const httrackp *opt) {
|
||||
TStamp ltime = time_local();
|
||||
int r;
|
||||
|
||||
timeout = ftp_wait_left(opt, timeout);
|
||||
|
||||
#if FTP_DEBUG
|
||||
printf("\x0dWaiting for data ");
|
||||
fflush(stdout);
|
||||
#endif
|
||||
while((!(r = check_socket(soc)))
|
||||
&& (((int) ((TStamp) (time_local() - ltime))) < timeout)) {
|
||||
/* A stop raised by the engine ends the wait on the next 100ms tick instead of
|
||||
after the full timeout, so teardown does not sit on a silent server. */
|
||||
while ((!(r = check_socket(soc))) && (back == NULL || !back->stop_ftp) &&
|
||||
(((int) ((TStamp) (time_local() - ltime))) < timeout)) {
|
||||
Sleep(100);
|
||||
#if FTP_DEBUG
|
||||
printf(".");
|
||||
|
||||
47
src/htsftp.h
47
src/htsftp.h
@@ -55,13 +55,20 @@ typedef struct FTPDownloadStruct FTPDownloadStruct;
|
||||
struct FTPDownloadStruct {
|
||||
lien_back *pBack;
|
||||
httrackp *pOpt;
|
||||
FTPDownloadStruct *pNext; /* live-worker list, owned by htsftp.c */
|
||||
};
|
||||
|
||||
/* Library internal definictions */
|
||||
#ifdef HTS_INTERNAL_BYTECODE
|
||||
/* Capacity of every FTP control-line buffer; send_line() adds the CRLF. */
|
||||
#define FTP_LINE_SIZE 1024
|
||||
|
||||
#if USE_BEGINTHREAD
|
||||
void launch_ftp(FTPDownloadStruct * params);
|
||||
void back_launch_ftp(void *pP);
|
||||
/* Cancel every live FTP worker and block until each stops touching its backlog
|
||||
slot and opt. Call before freeing either. */
|
||||
void ftp_stop_workers(void);
|
||||
#else
|
||||
void launch_ftp(FTPDownloadStruct * params, char *path, char *exec);
|
||||
int back_launch_ftp(FTPDownloadStruct * params);
|
||||
@@ -69,23 +76,43 @@ int back_launch_ftp(FTPDownloadStruct * params);
|
||||
|
||||
int run_launch_ftp(FTPDownloadStruct * params);
|
||||
int send_line(T_SOC soc, const char *data);
|
||||
int get_ftp_line(T_SOC soc, char *line, size_t line_size, int timeout);
|
||||
/* Read one control-channel reply into line[line_size]. Returns 0 on error.
|
||||
Optional back ends the wait on a stop, opt clips it to --max-time. */
|
||||
int get_ftp_line(lien_back *back, T_SOC soc, char *line, size_t line_size,
|
||||
int timeout, const httrackp *opt);
|
||||
/* Split a "user[:pass]@" prefix (end = jump_identification result) into
|
||||
bounded, NUL-terminated user/pass buffers, truncating to fit.
|
||||
NUL-terminated user/pass buffers. Returns HTS_FALSE and empties both when a
|
||||
field does not fit, as a clipped one would name another account.
|
||||
Both sizes must be nonzero. */
|
||||
void ftp_split_userpass(const char *src, const char *end, char *user,
|
||||
size_t user_size, char *pass, size_t pass_size);
|
||||
/* Build "<verb> <path>" into line[line_size], truncating to fit. The path is
|
||||
quoted whenever a bare one would give the server a second token; it must
|
||||
already have been screened for control bytes. */
|
||||
void ftp_command(char *line, size_t line_size, const char *verb,
|
||||
const char *path);
|
||||
hts_boolean ftp_split_userpass(const char *src, const char *end, char *user,
|
||||
size_t user_size, char *pass, size_t pass_size);
|
||||
/* ftp_split_userpass() into the caller's fixed buffers; a buffer too wide for
|
||||
its "USER <user>" line would be clipped again when the command is built. */
|
||||
#define ftp_split_userpass_buf(src, end, user, pass) \
|
||||
(HTS_COMPILE_ASSERT(sizeof(user) + sizeof("USER ") - 1 <= FTP_LINE_SIZE && \
|
||||
sizeof(pass) + sizeof("PASS ") - 1 <= FTP_LINE_SIZE), \
|
||||
ftp_split_userpass((src), (end), (user), sizeof(user), (pass), \
|
||||
sizeof(pass)))
|
||||
/* Build "<verb> <path>" into line[line_size]. The path is quoted whenever a
|
||||
bare one would give the server a second token; it must already have been
|
||||
screened for control bytes. Returns HTS_FALSE and empties line when the
|
||||
command does not fit, as a clipped one would name a different file. */
|
||||
hts_boolean ftp_command(char *line, size_t line_size, const char *verb,
|
||||
const char *path);
|
||||
/* ftp_command() into a control line of the one capacity every FTP buffer has;
|
||||
anything narrower fails the build rather than refusing a path that fits. */
|
||||
#define ftp_command_line(line, verb, path) \
|
||||
(HTS_COMPILE_ASSERT(sizeof(line) == FTP_LINE_SIZE), \
|
||||
ftp_command((line), sizeof(line), (verb), (path)))
|
||||
T_SOC get_datasocket(char *to_send, size_t to_send_size);
|
||||
int stop_ftp(lien_back * back);
|
||||
char *linejmp(char *line);
|
||||
int check_socket(T_SOC soc);
|
||||
int check_socket_connect(T_SOC soc);
|
||||
int wait_socket_receive(T_SOC soc, int timeout);
|
||||
/* Wait up to timeout seconds for soc to become readable. Optional back ends the
|
||||
wait on a stop, opt clips it to --max-time. */
|
||||
int wait_socket_receive(lien_back *back, T_SOC soc, int timeout,
|
||||
const httrackp *opt);
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
@@ -43,8 +43,8 @@ Please visit our Website: http://www.httrack.com
|
||||
configure.ac, decoupled from these). VERSION is the display form, VERSIONID
|
||||
the dotted numeric form, AFF_VERSION the short form shown in footers,
|
||||
LIB_VERSION the data/cache format generation. */
|
||||
#define HTTRACK_VERSION "3.49-17"
|
||||
#define HTTRACK_VERSIONID "3.49.17"
|
||||
#define HTTRACK_VERSION "3.49-19"
|
||||
#define HTTRACK_VERSIONID "3.49.19"
|
||||
#define HTTRACK_AFF_VERSION "3.x"
|
||||
#define HTTRACK_LIB_VERSION "2.0"
|
||||
|
||||
@@ -212,8 +212,8 @@ Please visit our Website: http://www.httrack.com
|
||||
/* Maximum URL length, in bytes. Callers size URL/path string buffers to this;
|
||||
anything longer is rejected. */
|
||||
#define HTS_URLMAXSIZE 1024
|
||||
/* Maximum command-line argument length, in bytes (kept >= HTS_URLMAXSIZE*2 so
|
||||
an addr+path pair always fits). */
|
||||
/* Command-line argument cap, in bytes: an argument this long or longer is
|
||||
rejected. A buffer holding a message built around one adds +256. */
|
||||
#define HTS_CDLMAXSIZE 1024
|
||||
/* MIME-type buffer contract (htsblk.contenttype/charset/contentencoding); holds
|
||||
the longest registered MIME type, the Office OOXML ones reaching 73 chars */
|
||||
@@ -273,6 +273,9 @@ typedef int hts_tristate;
|
||||
/* True when A is a non-NULL, non-empty string. */
|
||||
#define strnotempty(A) (((A) != NULL && (A)[0] != '\0'))
|
||||
|
||||
/* Compile-time check, usable as an expression. */
|
||||
#define HTS_COMPILE_ASSERT(cond) ((void) sizeof(char[(cond) ? 1 : -1]))
|
||||
|
||||
/* 'inline' where the dialect supports it (C++), nothing in plain C. */
|
||||
#ifdef __cplusplus
|
||||
#define HTS_INLINE inline
|
||||
|
||||
82
src/htslib.c
82
src/htslib.c
@@ -47,6 +47,7 @@ Please visit our Website: http://www.httrack.com
|
||||
#include "htsbauth.h"
|
||||
#include "htsthread.h"
|
||||
#include "htsback.h"
|
||||
#include "htsftp.h"
|
||||
#include "htswrap.h"
|
||||
#include "htsmd5.h"
|
||||
#include "htsmodules.h"
|
||||
@@ -2121,6 +2122,30 @@ htsblk http_test(httrackp * opt, const char *adr, const char *fil, char *loc) {
|
||||
return retour;
|
||||
}
|
||||
|
||||
hts_boolean socket_set_nonblocking(T_SOC soc, hts_boolean nonblocking) {
|
||||
#ifdef _WIN32
|
||||
unsigned long p = nonblocking ? 1 : 0;
|
||||
|
||||
return ioctlsocket(soc, FIONBIO, &p) == 0 ? HTS_TRUE : HTS_FALSE;
|
||||
#else
|
||||
const int flags = fcntl(soc, F_GETFL, 0);
|
||||
const int wanted = nonblocking ? (flags | O_NONBLOCK) : (flags & ~O_NONBLOCK);
|
||||
|
||||
if (flags == -1 || fcntl(soc, F_SETFL, wanted) == -1)
|
||||
return HTS_FALSE;
|
||||
return HTS_TRUE;
|
||||
#endif
|
||||
}
|
||||
|
||||
int connect_socket_error(T_SOC soc) {
|
||||
int soerr = 0;
|
||||
socklen_t len = (socklen_t) sizeof(soerr);
|
||||
|
||||
if (getsockopt(soc, SOL_SOCKET, SO_ERROR, (char *) &soerr, &len) != 0)
|
||||
return -1;
|
||||
return soerr;
|
||||
}
|
||||
|
||||
// Crée un lien (http) vers une adresse internet iadr
|
||||
// retour: structure (adresse, taille, message si erreur (si !adr))
|
||||
// peut ouvrir avec des connect() non bloquants: waitconnect=0/1
|
||||
@@ -2269,24 +2294,17 @@ T_SOC newhttp_addr(httrackp *opt, const char *_iadr, htsblk *retour, int port,
|
||||
|
||||
// connexion non bloquante?
|
||||
if (!waitconnect) {
|
||||
if (!socket_set_nonblocking(soc, HTS_TRUE)) {
|
||||
#ifdef _WIN32
|
||||
unsigned long p = 1; // non bloquant
|
||||
if (ioctlsocket(soc, FIONBIO, &p)) {
|
||||
const int last_errno = WSAGetLastError();
|
||||
#else
|
||||
const int last_errno = errno;
|
||||
#endif
|
||||
snprintf(retour->msg, sizeof(retour->msg),
|
||||
"Non-blocking socket failed: %s", strerror(last_errno));
|
||||
deletesoc(soc);
|
||||
return INVALID_SOCKET;
|
||||
}
|
||||
#else
|
||||
const int flags = fcntl(soc, F_GETFL, 0);
|
||||
if (flags == -1 || fcntl(soc, F_SETFL, flags | O_NONBLOCK) == -1) {
|
||||
snprintf(retour->msg, sizeof(retour->msg),
|
||||
"Non-blocking socket failed: %s", strerror(errno));
|
||||
deletesoc(soc);
|
||||
return INVALID_SOCKET;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
// Connexion au serveur lui même
|
||||
#if HDEBUG
|
||||
@@ -5184,20 +5202,20 @@ static void dns_resolve_thread(void *arg) {
|
||||
dns_job_release(job);
|
||||
}
|
||||
|
||||
/* Resolve hostname on a worker thread, giving up after timeout seconds.
|
||||
Returns the address count, or -1 on timeout -- distinct from 0 ("does not
|
||||
resolve"), which is a real answer and gets negative-cached. */
|
||||
static int hts_dns_resolve_nocache_list_bounded(const char *hostname,
|
||||
SOCaddr *const out,
|
||||
const int max,
|
||||
const int timeout,
|
||||
const char **error) {
|
||||
/* Resolve hostname on a worker thread, giving up after timeout seconds or as
|
||||
soon as *cancel (optional) is raised. Returns the address count, or -1 on
|
||||
timeout -- distinct from 0 ("does not resolve"), which is a real answer and
|
||||
gets negative-cached. */
|
||||
static int hts_dns_resolve_nocache_list_bounded(
|
||||
const char *hostname, SOCaddr *const out, const int max, const int timeout,
|
||||
const volatile hts_boolean *cancel, const char **error) {
|
||||
dns_resolve_job *job;
|
||||
TStamp deadline;
|
||||
int count = -1;
|
||||
int poll_ms = 1;
|
||||
|
||||
if (timeout <= 0) /* no bound asked for (--timeout 0) */
|
||||
/* no bound asked for (--timeout 0), and nobody to cut it short either */
|
||||
if (timeout <= 0 && cancel == NULL)
|
||||
return hts_dns_resolve_nocache_list(hostname, out, max, error);
|
||||
|
||||
job = calloct(1, sizeof(*job));
|
||||
@@ -5211,7 +5229,8 @@ static int hts_dns_resolve_nocache_list_bounded(const char *hostname,
|
||||
return hts_dns_resolve_nocache_list(hostname, out, max, error);
|
||||
}
|
||||
|
||||
deadline = mtime_local() + (TStamp) timeout * 1000;
|
||||
/* timeout <= 0 got here only for a cancellable resolve: no deadline then */
|
||||
deadline = timeout > 0 ? mtime_local() + (TStamp) timeout * 1000 : 0;
|
||||
for (;;) {
|
||||
hts_boolean done;
|
||||
|
||||
@@ -5224,7 +5243,8 @@ static int hts_dns_resolve_nocache_list_bounded(const char *hostname,
|
||||
*error = job->error;
|
||||
}
|
||||
hts_mutexrelease(&job->lock);
|
||||
if (done || mtime_local() >= deadline)
|
||||
if (done || (cancel != NULL && *cancel) ||
|
||||
(deadline != 0 && mtime_local() >= deadline))
|
||||
break;
|
||||
Sleep(poll_ms);
|
||||
if (poll_ms < 50) /* short first polls keep a fast resolve fast */
|
||||
@@ -5236,6 +5256,17 @@ static int hts_dns_resolve_nocache_list_bounded(const char *hostname,
|
||||
|
||||
int hts_dns_resolve_all(httrackp *opt, const char *iadr, SOCaddr *out, int max,
|
||||
const char **error) {
|
||||
assertf(opt != NULL);
|
||||
/* the mirror's stop flag cancels: a resolve behind a black hole otherwise
|
||||
holds a stopped crawl for the whole --timeout (#1073) */
|
||||
return hts_dns_resolve_all_bounded(opt, iadr, out, max, opt->timeout,
|
||||
&opt->state.stop, error);
|
||||
}
|
||||
|
||||
int hts_dns_resolve_all_bounded(httrackp *opt, const char *iadr, SOCaddr *out,
|
||||
int max, int timeout,
|
||||
const volatile hts_boolean *cancel,
|
||||
const char **error) {
|
||||
char BIGSTK host[HTS_URLMAXSIZE * 2];
|
||||
SOCaddr resolved[HTS_MAXADDRNUM];
|
||||
coucal cache;
|
||||
@@ -5274,7 +5305,7 @@ int hts_dns_resolve_all(httrackp *opt, const char *iadr, SOCaddr *out, int max,
|
||||
/* Resolve with no lock held: getaddrinfo can block for a long time, and
|
||||
state.lock also gates the stop request (#606). */
|
||||
count = hts_dns_resolve_nocache_list_bounded(host, resolved, HTS_MAXADDRNUM,
|
||||
opt->timeout, error);
|
||||
timeout, cancel, error);
|
||||
|
||||
#if HTS_WIDE_DEBUG
|
||||
DEBUG_W("gethostbyname done\n");
|
||||
@@ -6055,6 +6086,7 @@ HTSEXT_API httrackp *hts_create_opt(void) {
|
||||
opt->changes_state = NULL;
|
||||
opt->single_file = HTS_FALSE;
|
||||
opt->single_file_max_size = SINGLEFILE_DEFAULT_MAX_SIZE;
|
||||
opt->singlefile_state = NULL;
|
||||
StringCopy(opt->why_url, "");
|
||||
opt->pause_min_ms = 0;
|
||||
opt->pause_max_ms = 0;
|
||||
@@ -6126,6 +6158,9 @@ HTSEXT_API size_t hts_sizeof_opt(void) {
|
||||
|
||||
HTSEXT_API void hts_free_opt(httrackp * opt) {
|
||||
if (opt != NULL) {
|
||||
/* An FTP worker reads opt for its whole run, and not every caller drains
|
||||
its threads the way httrack.c does before getting here. */
|
||||
ftp_stop_workers();
|
||||
|
||||
/* Alocated callbacks */
|
||||
|
||||
@@ -6209,6 +6244,7 @@ HTSEXT_API void hts_free_opt(httrackp * opt) {
|
||||
StringFree(opt->warc_file);
|
||||
StringFree(opt->sitemap_url);
|
||||
hts_sitemap_free(opt); /* backstop: httpmirror's early-return paths */
|
||||
singlefile_free(opt);
|
||||
|
||||
hts_changes_free_opt(opt);
|
||||
|
||||
|
||||
19
src/htslib.h
19
src/htslib.h
@@ -214,6 +214,16 @@ int http_sendhead(httrackp * opt, t_cookie * cookie, int mode, const char *xsend
|
||||
int http_cookie_header(t_cookie *cookie, const char *domain, const char *path,
|
||||
char *dst, size_t dst_size);
|
||||
|
||||
/* Switch soc between blocking and non-blocking mode; false on failure, with
|
||||
errno (WSAGetLastError() on Windows) set. */
|
||||
hts_boolean socket_set_nonblocking(T_SOC soc, hts_boolean nonblocking);
|
||||
/* Pending-connect result for a non-blocking socket reported ready by select():
|
||||
0 = connected, >0 = the connect errno (refused, unreachable, ...), -1 if the
|
||||
probe itself failed. A failed connect is reported ready as well (writable on
|
||||
posix, exception set on winsock), so this is how success is told from failure
|
||||
without blocking. */
|
||||
int connect_socket_error(T_SOC soc);
|
||||
|
||||
T_SOC newhttp(httrackp * opt, const char *iadr, htsblk * retour, int port,
|
||||
int waitconnect);
|
||||
/* Like newhttp(), but connect to the addr_index-th resolved address of the host
|
||||
@@ -269,9 +279,16 @@ LLint http_xfread1(htsblk * r, int bufl);
|
||||
resolver order), returning the count (0 = does not resolve, negative-cached).
|
||||
Resolves once per host; later calls read the DNS cache. Must hold no lock
|
||||
(brackets opt->state.lock itself, never across the resolve). A miss resolves
|
||||
on a worker thread bounded by opt->timeout; a timeout reports 0, uncached. */
|
||||
on a worker thread bounded by opt->timeout and cut short by a mirror stop;
|
||||
either reports 0, uncached. */
|
||||
int hts_dns_resolve_all(httrackp *opt, const char *iadr, SOCaddr *out, int max,
|
||||
const char **error);
|
||||
/* Like hts_dns_resolve_all(), with the wait bounded by timeout seconds (<= 0:
|
||||
unbounded) and cut short as soon as *cancel, if given, is raised. */
|
||||
int hts_dns_resolve_all_bounded(httrackp *opt, const char *iadr, SOCaddr *out,
|
||||
int max, int timeout,
|
||||
const volatile hts_boolean *cancel,
|
||||
const char **error);
|
||||
HTS_INLINE SOCaddr *hts_dns_resolve2(httrackp *opt, const char *iadr,
|
||||
SOCaddr *const addr, const char **error);
|
||||
HTS_INLINE SOCaddr* hts_dns_resolve(httrackp * opt, const char *iadr,
|
||||
|
||||
52
src/htsnet.c
Normal file
52
src/htsnet.c
Normal file
@@ -0,0 +1,52 @@
|
||||
/* ------------------------------------------------------------ */
|
||||
/*
|
||||
HTTrack Website Copier, Offline Browser for Windows and Unix
|
||||
Copyright (C) 2026 Xavier Roche and other contributors
|
||||
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Ethical use: we kindly ask that you NOT use this software to harvest email
|
||||
addresses or to collect any other private information about people. Doing so
|
||||
would dishonor our work and waste the many hours we have spent on it.
|
||||
|
||||
Please visit our Website: http://www.httrack.com
|
||||
*/
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/* File: Out-of-line htsnet.h helpers, kept here so the installed */
|
||||
/* header needs nothing beyond strict ISO C */
|
||||
/* Author: Xavier Roche */
|
||||
/* ------------------------------------------------------------ */
|
||||
|
||||
#include "htsnet.h"
|
||||
|
||||
HTSNET_API void SOCaddr_inetntoa_(char *namebuf, size_t namebuflen,
|
||||
SOCaddr *const ss, const char *file,
|
||||
const int line) {
|
||||
assertf_(namebuf != NULL, file, line);
|
||||
assertf_(ss != NULL, file, line);
|
||||
|
||||
if (getnameinfo(&ss->m_addr.sa, sizeof(ss->m_addr), namebuf, namebuflen, NULL,
|
||||
0, NI_NUMERICHOST) == 0) {
|
||||
/* remove scope id(s) */
|
||||
char *const pos = strchr(namebuf, '%');
|
||||
if (pos != NULL) {
|
||||
*pos = '\0';
|
||||
}
|
||||
} else {
|
||||
namebuf[0] = '\0';
|
||||
}
|
||||
}
|
||||
31
src/htsnet.h
31
src/htsnet.h
@@ -273,25 +273,20 @@ static HTS_UNUSED socklen_t SOCaddr_copyaddr_(SOCaddr *const server,
|
||||
__LINE__); \
|
||||
} while (0)
|
||||
|
||||
/** Write the numeric (dotted/colon) host of ss into namebuf (capacity
|
||||
namebuflen), scope id stripped. On failure namebuf becomes "". */
|
||||
static HTS_UNUSED void SOCaddr_inetntoa_(char *namebuf, size_t namebuflen,
|
||||
SOCaddr *const ss, const char *file,
|
||||
const int line) {
|
||||
assertf_(namebuf != NULL, file, line);
|
||||
assertf_(ss != NULL, file, line);
|
||||
/* proxytrack compiles htsnet.c in rather than linking the library, and MSVC
|
||||
rejects a dllimport definition. */
|
||||
#ifdef HTS_NO_LIBHTTRACK
|
||||
#define HTSNET_API
|
||||
#else
|
||||
#define HTSNET_API HTSEXT_API
|
||||
#endif
|
||||
|
||||
if (getnameinfo(&ss->m_addr.sa, sizeof(ss->m_addr), namebuf, namebuflen, NULL,
|
||||
0, NI_NUMERICHOST) == 0) {
|
||||
/* remove scope id(s) */
|
||||
char *const pos = strchr(namebuf, '%');
|
||||
if (pos != NULL) {
|
||||
*pos = '\0';
|
||||
}
|
||||
} else {
|
||||
namebuf[0] = '\0';
|
||||
}
|
||||
}
|
||||
/** Write the numeric (dotted/colon) host of ss into namebuf (capacity
|
||||
namebuflen), scope id stripped. On failure namebuf becomes "". Out of line:
|
||||
getnameinfo() isn't declared to a strict-ISO translation unit (#1001). */
|
||||
HTSNET_API void SOCaddr_inetntoa_(char *namebuf, size_t namebuflen,
|
||||
SOCaddr *const ss, const char *file,
|
||||
const int line);
|
||||
|
||||
/** Numeric host of ss into namebuf (capacity namebuflen); "" on failure. */
|
||||
#define SOCaddr_inetntoa(namebuf, namebuflen, ss) \
|
||||
|
||||
13
src/htsopt.h
13
src/htsopt.h
@@ -222,7 +222,8 @@ typedef struct htsoptstate htsoptstate;
|
||||
struct htsoptstate {
|
||||
htsmutex lock; /**< guards this state block */
|
||||
/* */
|
||||
int stop; /**< set to request the mirror to stop */
|
||||
/** set to request the mirror to stop; volatile: polled without a lock */
|
||||
volatile int stop;
|
||||
int exit_xh;
|
||||
int back_add_stats;
|
||||
/* */
|
||||
@@ -256,7 +257,8 @@ struct htsoptstate {
|
||||
unsigned int debug_state;
|
||||
unsigned int tmpnameid; /**< counter for temporary file names */
|
||||
int is_ended; /**< mirror has finished */
|
||||
void *warc; /**< open WARC writer (warc_writer*), or NULL */
|
||||
void *warc; /**< WARC writer (warc_writer*), or NULL, or the WARC_DISABLED
|
||||
sentinel (htswarc.c) */
|
||||
};
|
||||
|
||||
/* Library handles */
|
||||
@@ -526,6 +528,7 @@ struct httrackp {
|
||||
// store library handles
|
||||
htslibhandles libHandles; /**< loaded external module handles */
|
||||
//
|
||||
/* Live state, not options: copy_htsopt must leave it alone. */
|
||||
htsoptstate state; /**< embedded live engine state */
|
||||
String strip_query; /**< query keys to drop when deduping URLs (-strip-query);
|
||||
appended at the tail to keep field offsets stable */
|
||||
@@ -564,6 +567,7 @@ struct httrackp {
|
||||
rather than in htsoptstate because that struct is embedded by value, so
|
||||
growing it would shift every httrackp field declared after it. */
|
||||
void *sitemap_state; /**< hts_sitemap_state*, or NULL. Tail: ABI */
|
||||
void *singlefile_state; /**< hts_singlefile_state*, or NULL. Tail: ABI */
|
||||
};
|
||||
|
||||
/* Running statistics for a mirror. */
|
||||
@@ -723,7 +727,8 @@ struct lien_url {
|
||||
int testmode; /**< test only: send just a HEAD */
|
||||
hts_boolean
|
||||
refetch_whole; /**< force a whole-file GET, ignoring any partial/temp-ref
|
||||
resume, so a rejected 206 can't loop (#581) */
|
||||
resume (#581); doubles as the latch spending this link's one
|
||||
free restart, so copy it wherever retry is copied (#1052) */
|
||||
};
|
||||
|
||||
/* A file being fetched in the background. */
|
||||
@@ -767,7 +772,7 @@ struct lien_back {
|
||||
LLint chunk_blocksize; /**< data size declared by the chunk */
|
||||
LLint compressed_size; /**< compressed size (stats only) */
|
||||
char info[256]; /**< status text, e.g. for FTP */
|
||||
int stop_ftp; /**< stop flag for FTP */
|
||||
volatile int stop_ftp; /**< stop flag for FTP, polled without a lock */
|
||||
int finalized; /**< finalized (memory optimization) */
|
||||
int early_add; /**< was added before the link heap saw it */
|
||||
#if DEBUG_CHECKINT
|
||||
|
||||
@@ -59,6 +59,7 @@ Please visit our Website: http://www.httrack.com
|
||||
|
||||
// parser
|
||||
#include "htsparse.h"
|
||||
#include "htssinglefile.h"
|
||||
#include "htsback.h"
|
||||
|
||||
// arrays
|
||||
@@ -398,6 +399,7 @@ int htsparse(htsmoduleStruct * str, htsmoduleStructExtended * stre) {
|
||||
FILE *fp = NULL; // fichier écrit localement
|
||||
const char *html = r->adr; // pointeur (on parcours)
|
||||
const char *lastsaved; // adresse du dernier octet sauvé + 1
|
||||
hts_boolean sf_may_mark = HTS_FALSE; // --single-file: mark this one?
|
||||
|
||||
hts_log_print(opt, LOG_DEBUG, "scanning file %s%s (%s)..", urladr(), urlfil(),
|
||||
savename());
|
||||
@@ -413,6 +415,10 @@ int htsparse(htsmoduleStruct * str, htsmoduleStructExtended * stre) {
|
||||
r->adr[i] = ' ';
|
||||
}
|
||||
}
|
||||
/* Only chance can put this run's secret in a fetched document. */
|
||||
if (opt->single_file) {
|
||||
sf_may_mark = singlefile_may_mark(opt, r->adr, (size_t) r->size);
|
||||
}
|
||||
}
|
||||
|
||||
// Indexing!
|
||||
@@ -1831,6 +1837,12 @@ int htsparse(htsmoduleStruct * str, htsmoduleStructExtended * stre) {
|
||||
int quoteinscript = 0;
|
||||
int noquote = 0;
|
||||
const char *tag_attr_start = html;
|
||||
/* Kept because the value scan below clears intag on an unquoted
|
||||
value ending at '>', and --single-file must still know the tag.
|
||||
inscript_locked marks a whole CSS or JS file, the one context
|
||||
that legitimately has no tag. */
|
||||
const char *const sf_tag = intag_start_valid ? intag_name : NULL;
|
||||
const int sf_tagless_body = inscript_locked;
|
||||
|
||||
// si nofollow ou un stop a été déclenché, réécrire tous les liens en externe
|
||||
if ((nofollow)
|
||||
@@ -3100,6 +3112,19 @@ int htsparse(htsmoduleStruct * str, htsmoduleStructExtended * stre) {
|
||||
|
||||
if ((opt->getmode & HTS_GETMODE_HTML) && (ptr > 0)) {
|
||||
|
||||
/* --single-file: tag the reference for the
|
||||
end-of-mirror pass. A fragment, so a mirror left
|
||||
marked by an interrupted run still browses. */
|
||||
/* A NULL tag reads as "a CSS or JS body", so a tag
|
||||
we cannot name must not reach the classifier. */
|
||||
const char sf_class =
|
||||
(opt->single_file && sf_may_mark && !in_media &&
|
||||
p_type == 0 && !p_searchMETAURL &&
|
||||
(sf_tag != NULL || sf_tagless_body))
|
||||
? singlefile_ref_class(sf_tag, tag_attr_start)
|
||||
: 0;
|
||||
const size_t sf_start = TypedArraySize(output_buffer);
|
||||
|
||||
// écrire le lien modifié, relatif
|
||||
// Note: escape all chars, even >127 (no UTF)
|
||||
HT_ADD_HTMLESCAPED_FULL(tempo);
|
||||
@@ -3113,6 +3138,28 @@ int htsparse(htsmoduleStruct * str, htsmoduleStructExtended * stre) {
|
||||
HT_ADD_HTMLESCAPED(a);
|
||||
}
|
||||
}
|
||||
|
||||
/* Measured on the appended bytes, not tempo: the
|
||||
escape can change the length. The query sits inside
|
||||
the span so inlining drops it. */
|
||||
if (sf_class != 0) {
|
||||
char sf_mark[SINGLEFILE_MARK_MAX];
|
||||
|
||||
/* SINGLEFILE_MAX_SPAN is derived from these two
|
||||
buffers, so a resize must move it: divide by zero
|
||||
here rather than silently stop marking. */
|
||||
enum {
|
||||
sf_span_fits =
|
||||
1 /
|
||||
(sizeof(tempo) * HTS_HTMLESCAPE_FULL_MAXEXP +
|
||||
sizeof(lien) * HTS_HTMLESCAPE_MAXEXP <=
|
||||
SINGLEFILE_MAX_SPAN)
|
||||
};
|
||||
|
||||
HT_ADD(singlefile_mark(
|
||||
opt, sf_mark, sizeof(sf_mark), sf_class,
|
||||
TypedArraySize(output_buffer) - sf_start));
|
||||
}
|
||||
}
|
||||
lastsaved = eadr - 1; // dernier écrit+1 (enfin euh apres on fait un ++ alors hein)
|
||||
} else {
|
||||
@@ -3622,6 +3669,9 @@ int hts_mirror_check_moved(htsmoduleStruct * str,
|
||||
heap_top()->pass2 =
|
||||
max(heap(ptr)->pass2, numero_passe);
|
||||
heap_top()->retry = heap(ptr)->retry;
|
||||
// same transfer under another name: carry the restart latch
|
||||
// too, else a 206/alias alternation restarts for free
|
||||
heap_top()->refetch_whole = heap(ptr)->refetch_whole;
|
||||
heap_top()->premier = heap(ptr)->premier;
|
||||
heap_top()->precedent = heap(ptr)->precedent;
|
||||
} else { // oups erreur, plus de mémoire!!
|
||||
@@ -3728,6 +3778,7 @@ int hts_mirror_check_moved(htsmoduleStruct * str,
|
||||
heap_top()->depth = heap(ptr)->depth;
|
||||
heap_top()->pass2 = max(heap(ptr)->pass2, numero_passe);
|
||||
heap_top()->retry = heap(ptr)->retry;
|
||||
heap_top()->refetch_whole = heap(ptr)->refetch_whole;
|
||||
heap_top()->premier = heap(ptr)->premier;
|
||||
heap_top()->precedent = ptr;
|
||||
error = 1;
|
||||
@@ -3802,8 +3853,15 @@ int hts_mirror_check_moved(htsmoduleStruct * str,
|
||||
break;
|
||||
}
|
||||
|
||||
/* Restarting a resume the server refused is not a retry of a failed
|
||||
transfer: free, but latched to one per link so an always-unusable
|
||||
server cannot loop (#1052). */
|
||||
const hts_boolean restart_whole =
|
||||
r->refetch_wholefile && !heap(ptr)->refetch_whole;
|
||||
|
||||
if (strcmp(heap(ptr)->fil, "/primary") != 0) { // no primary (internal page 0)
|
||||
if ((heap(ptr)->retry <= 0) || (!can_retry)) { // retry épuisés (ou retry impossible)
|
||||
// give up: no free restart left, and no budget (or nothing to retry)
|
||||
if (!restart_whole && ((heap(ptr)->retry <= 0) || (!can_retry))) {
|
||||
if ((opt->retry > 0) && (can_retry)) {
|
||||
hts_log_print(opt, LOG_ERROR,
|
||||
"\"%s\" (%d) after %d retries at link %s%s (from %s%s)",
|
||||
@@ -3853,23 +3911,32 @@ int hts_mirror_check_moved(htsmoduleStruct * str,
|
||||
}
|
||||
}
|
||||
|
||||
} else { // retry!!
|
||||
hts_log_print(opt, LOG_NOTICE,
|
||||
"Retry after error %d (%s) at link %s%s (from %s%s)",
|
||||
r->statuscode, r->msg, urladr(), urlfil(),
|
||||
heap(heap(ptr)->precedent)->adr,
|
||||
heap(heap(ptr)->precedent)->fil);
|
||||
// redemander fichier
|
||||
} else { // retry, or a refused-resume restart
|
||||
if (restart_whole) {
|
||||
hts_log_print(opt, LOG_NOTICE,
|
||||
"Restarting whole file after error %d (%s) at link "
|
||||
"%s%s (from %s%s)",
|
||||
r->statuscode, r->msg, urladr(), urlfil(),
|
||||
heap(heap(ptr)->precedent)->adr,
|
||||
heap(heap(ptr)->precedent)->fil);
|
||||
} else {
|
||||
hts_log_print(opt, LOG_NOTICE,
|
||||
"Retry after error %d (%s) at link %s%s (from %s%s)",
|
||||
r->statuscode, r->msg, urladr(), urlfil(),
|
||||
heap(heap(ptr)->precedent)->adr,
|
||||
heap(heap(ptr)->precedent)->fil);
|
||||
}
|
||||
// re-request the file
|
||||
if (hts_record_link(opt, urladr(), urlfil(), savename(), "", "", codebase)) {
|
||||
heap_top()->testmode = heap(ptr)->testmode; // mode test?
|
||||
heap_top()->link_import = 0; // pas mode import
|
||||
heap_top()->depth = heap(ptr)->depth;
|
||||
heap_top()->pass2 = max(heap(ptr)->pass2, numero_passe);
|
||||
heap_top()->retry = heap(ptr)->retry - 1; // moins 1 retry!
|
||||
heap_top()->retry =
|
||||
restart_whole ? heap(ptr)->retry : heap(ptr)->retry - 1;
|
||||
heap_top()->premier = heap(ptr)->premier;
|
||||
heap_top()->precedent = heap(ptr)->precedent;
|
||||
// a rejected resume (unusable 206) must refetch whole, no Range
|
||||
// (#581)
|
||||
// refetch whole with no Range, and latch out a second free restart
|
||||
heap_top()->refetch_whole = r->refetch_wholefile;
|
||||
} else { // oups erreur, plus de mémoire!!
|
||||
return 0;
|
||||
@@ -4333,7 +4400,8 @@ int hts_mirror_wait_for_next_file(htsmoduleStruct * str,
|
||||
}
|
||||
|
||||
if (!opt->verbosedisplay) {
|
||||
if (!opt->quiet) {
|
||||
/* The spinner overwrites itself; nothing to overwrite off a terminal. */
|
||||
if (!opt->quiet && hts_stdout_isterminal()) {
|
||||
static int roll = 0; /* static: ok */
|
||||
|
||||
roll = (roll + 1) % 4;
|
||||
@@ -4343,10 +4411,10 @@ int hts_mirror_wait_for_next_file(htsmoduleStruct * str,
|
||||
} else if (opt->verbosedisplay == HTS_VERBOSE_SIMPLE) {
|
||||
if (b >= 0) {
|
||||
if (back[b].r.statuscode == HTTP_OK)
|
||||
printf("%d/%d: %s%s (" LLintP " bytes) - OK\33[K\r", ptr, opt->lien_tot,
|
||||
printf("%d/%d: %s%s (" LLintP " bytes) - OK\n", ptr, opt->lien_tot,
|
||||
back[b].url_adr, back[b].url_fil, (LLint) back[b].r.size);
|
||||
else
|
||||
printf("%d/%d: %s%s (" LLintP " bytes) - %d\33[K\r", ptr, opt->lien_tot,
|
||||
printf("%d/%d: %s%s (" LLintP " bytes) - %d\n", ptr, opt->lien_tot,
|
||||
back[b].url_adr, back[b].url_fil, (LLint) back[b].r.size,
|
||||
back[b].r.statuscode);
|
||||
} else {
|
||||
|
||||
101
src/htsrandom.c
Normal file
101
src/htsrandom.c
Normal file
@@ -0,0 +1,101 @@
|
||||
/* ------------------------------------------------------------ */
|
||||
/*
|
||||
HTTrack Website Copier, Offline Browser for Windows and Unix
|
||||
Copyright (C) 2026 Xavier Roche and other contributors
|
||||
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Ethical use: we kindly ask that you NOT use this software to harvest email
|
||||
addresses or to collect any other private information about people. Doing so
|
||||
would dishonor our work and waste the many hours we have spent on it.
|
||||
|
||||
Please visit our Website: http://www.httrack.com
|
||||
*/
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/* System CSPRNG. See htsrandom.h. */
|
||||
/* ------------------------------------------------------------ */
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#include "htsrandom.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#ifdef HAVE_SYS_RANDOM_H
|
||||
#include <sys/random.h>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
/* RtlGenRandom, resolved at runtime so no import library is needed. */
|
||||
typedef BOOLEAN(WINAPI *hts_rtlgenrandom_t)(PVOID buffer, ULONG length);
|
||||
#endif
|
||||
|
||||
hts_boolean hts_random_bytes(void *buf, size_t len) {
|
||||
#ifdef _WIN32
|
||||
hts_boolean ok = HTS_FALSE;
|
||||
HMODULE dll = LoadLibraryA("advapi32.dll");
|
||||
|
||||
if (dll != NULL) {
|
||||
hts_rtlgenrandom_t gen =
|
||||
(hts_rtlgenrandom_t) GetProcAddress(dll, "SystemFunction036");
|
||||
|
||||
/* the ULONG cast must not truncate; callers ask for a few dozen bytes */
|
||||
if (gen != NULL && len <= 0x10000 && gen(buf, (ULONG) len)) {
|
||||
ok = HTS_TRUE;
|
||||
}
|
||||
FreeLibrary(dll);
|
||||
}
|
||||
return ok;
|
||||
#else
|
||||
unsigned char *const p = (unsigned char *) buf;
|
||||
size_t got = 0;
|
||||
FILE *fp;
|
||||
|
||||
#ifdef HAVE_GETRANDOM
|
||||
while (got < len) {
|
||||
const ssize_t n = getrandom(p + got, len - got, 0);
|
||||
|
||||
if (n < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
break; /* pre-3.17 kernel or a seccomp filter: try /dev/urandom */
|
||||
}
|
||||
got += (size_t) n;
|
||||
}
|
||||
if (got == len)
|
||||
return HTS_TRUE;
|
||||
#endif
|
||||
fp = fopen("/dev/urandom", "rb");
|
||||
if (fp == NULL)
|
||||
return HTS_FALSE;
|
||||
while (got < len) {
|
||||
const size_t n = fread(p + got, 1, len - got, fp);
|
||||
|
||||
if (n == 0) /* a short read is a hard failure, not partial credit */
|
||||
break;
|
||||
got += n;
|
||||
}
|
||||
fclose(fp);
|
||||
return got == len ? HTS_TRUE : HTS_FALSE;
|
||||
#endif
|
||||
}
|
||||
52
src/htsrandom.h
Normal file
52
src/htsrandom.h
Normal file
@@ -0,0 +1,52 @@
|
||||
/* ------------------------------------------------------------ */
|
||||
/*
|
||||
HTTrack Website Copier, Offline Browser for Windows and Unix
|
||||
Copyright (C) 2026 Xavier Roche and other contributors
|
||||
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Ethical use: we kindly ask that you NOT use this software to harvest email
|
||||
addresses or to collect any other private information about people. Doing so
|
||||
would dishonor our work and waste the many hours we have spent on it.
|
||||
|
||||
Please visit our Website: http://www.httrack.com
|
||||
*/
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/* System CSPRNG. Its own file because htsserver links libhttrack but cannot
|
||||
see a hidden symbol inside it, and both need this one. */
|
||||
/* ------------------------------------------------------------ */
|
||||
|
||||
#ifndef HTS_RANDOM_DEFH
|
||||
#define HTS_RANDOM_DEFH
|
||||
|
||||
#include "htsglobal.h"
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Fill buf with len cryptographically strong random bytes. HTS_FALSE when no
|
||||
such source is available: the caller must fail, never fall back to rand(). */
|
||||
hts_boolean hts_random_bytes(void *buf, size_t len);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -112,7 +112,8 @@ static HTS_UNUSED void abortf_(const char *exp, const char *file, int line) {
|
||||
#define HTS_IS_CHAR_BUFFER(VAR) \
|
||||
(__builtin_types_compatible_p(__typeof__(VAR), char[]))
|
||||
#else
|
||||
/* Note: a bit lame as char[8] won't be seen. */
|
||||
/* An array of exactly pointer size reads as a pointer and loses its bound:
|
||||
keep buff() destinations off char[8] (Win64) and char[4] (Win32). */
|
||||
#define HTS_IS_CHAR_BUFFER(VAR) (sizeof(VAR) != sizeof(char *))
|
||||
#endif
|
||||
#define HTS_IS_NOT_CHAR_BUFFER(VAR) (!HTS_IS_CHAR_BUFFER(VAR))
|
||||
|
||||
1143
src/htsselftest.c
1143
src/htsselftest.c
File diff suppressed because it is too large
Load Diff
1092
src/htssinglefile.c
1092
src/htssinglefile.c
File diff suppressed because it is too large
Load Diff
@@ -38,6 +38,7 @@ Please visit our Website: http://www.httrack.com
|
||||
#ifndef HTS_SINGLEFILE_DEFH
|
||||
#define HTS_SINGLEFILE_DEFH
|
||||
|
||||
#include "htscore.h" /* HTS_HTMLESCAPE_*_MAXEXP */
|
||||
#include "htsopt.h"
|
||||
#include "htsstrings.h"
|
||||
|
||||
@@ -55,18 +56,74 @@ extern "C" {
|
||||
so a few hundred bytes of hostile CSS can otherwise ask for gigabytes. */
|
||||
#define SINGLEFILE_MAX_PAGE_SIZE (64 * 1024 * 1024)
|
||||
|
||||
/* Rewrite every HTML page the mirror produced. No-op unless opt->single_file;
|
||||
call once the tree is final, after the update purge. */
|
||||
/* The mark htsparse appends to a saved reference the pass may inline:
|
||||
|
||||
#!<16-hex secret>.<class>.<len>
|
||||
|
||||
A fragment, so a mirror left marked by an interrupted run still browses.
|
||||
The secret is 64 CSPRNG bits drawn once per run and never written to disk,
|
||||
which is what makes the mark unforgeable: a site cannot spell one, so no
|
||||
sanitiser has to keep hostile bytes away from it. <len> is the byte length
|
||||
of the reference text immediately preceding the mark, so the pass never has
|
||||
to guess where that reference starts. <class> is the context htsparse saw,
|
||||
checked against the resolved type so a mismatch fails loudly. */
|
||||
#define SINGLEFILE_MARK_INTRO "#!"
|
||||
#define SINGLEFILE_SECRET_HEX 16
|
||||
|
||||
/* Bytes before the ".<class>.<len>" tail. */
|
||||
#define SINGLEFILE_INTRO_LEN \
|
||||
(sizeof(SINGLEFILE_MARK_INTRO) - 1 + SINGLEFILE_SECRET_HEX)
|
||||
|
||||
/* <class>: what the referencing context expects. */
|
||||
#define SINGLEFILE_CLASS_ANY '-'
|
||||
#define SINGLEFILE_CLASS_CSS 'c'
|
||||
#define SINGLEFILE_CLASS_JS 'j'
|
||||
|
||||
/* Enough for the intro, the secret, both separators and a 20-digit length. */
|
||||
#define SINGLEFILE_MARK_MAX 64
|
||||
|
||||
/* Longest <len> a mark may carry, which is what htsparse can append for one
|
||||
reference: HTS_URLMAXSIZE*2 of savename through escape_for_html_print_full,
|
||||
plus as much query through escape_for_html_print. The emit site asserts the
|
||||
derivation against its own buffers. */
|
||||
#define SINGLEFILE_MAX_SPAN \
|
||||
(HTS_URLMAXSIZE * 2 * (HTS_HTMLESCAPE_FULL_MAXEXP + HTS_HTMLESCAPE_MAXEXP))
|
||||
|
||||
/* Release this run's secret. */
|
||||
void singlefile_free(httrackp *opt);
|
||||
|
||||
/* "#!<secret>" for this run, or NULL if no CSPRNG was available (in which case
|
||||
nothing may be marked). Draws the secret on first use. */
|
||||
const char *singlefile_intro(httrackp *opt);
|
||||
|
||||
/* HTS_FALSE if [body,len) already contains this run's intro, in which case the
|
||||
document must not be marked: RFC 2046 gives the generator the same duty for
|
||||
a MIME boundary. Only a 2^-64 coincidence can trip it, since the intro is
|
||||
not something fetched content can spell. */
|
||||
hts_boolean singlefile_may_mark(httrackp *opt, const char *body, size_t len);
|
||||
|
||||
/* Write the mark for a reference of reflen bytes into buf; returns buf, left
|
||||
empty for a reflen the pass could not read back. */
|
||||
const char *singlefile_mark(httrackp *opt, char *buf, size_t bufsize, char cls,
|
||||
size_t reflen);
|
||||
|
||||
/* The class a reference in this context may inline as, or 0 to leave it alone.
|
||||
tag_name points just past the '<' of the enclosing start tag, or NULL when
|
||||
there is none (inside a stylesheet or a script); attr at the attribute name.
|
||||
Everything htsparse detects is inlinable unless it names a page. */
|
||||
char singlefile_ref_class(const char *tag_name, const char *attr);
|
||||
|
||||
/* Rewrite every HTML page the mirror produced, then strip the marks left in
|
||||
the assets. No-op unless opt->single_file; call once the tree is final,
|
||||
after the update purge. */
|
||||
void singlefile_process_mirror(httrackp *opt);
|
||||
|
||||
/* Rewrite one HTML document held in memory, appending the result to out.
|
||||
root is the mirror directory that references may not escape; page_path is
|
||||
the document's own path under it (both UTF-8, '/' or native separators).
|
||||
page_budget caps the total inlined bytes, since nested @import fans out
|
||||
/* Expand the marks in the document held in memory, appending the result to
|
||||
out. root is the mirror directory that references may not escape; page_path
|
||||
is the document's own path under it (both UTF-8, '/' or native separators).
|
||||
page_budget caps the total inlined bytes, since a nested @import fans out
|
||||
multiplicatively; the mirror pass passes SINGLEFILE_MAX_PAGE_SIZE.
|
||||
Returns HTS_TRUE if at least one reference was replaced; out may still
|
||||
differ from the input when that is HTS_FALSE, since a style or srcset value
|
||||
is re-serialized in place. */
|
||||
Returns HTS_TRUE if at least one reference was replaced. */
|
||||
hts_boolean singlefile_rewrite_html(httrackp *opt, const char *root,
|
||||
const char *page_path, const char *html,
|
||||
size_t html_len, LLint page_budget,
|
||||
|
||||
@@ -42,6 +42,7 @@ Please visit our Website: http://www.httrack.com
|
||||
#include "htscharset.h"
|
||||
#ifdef _WIN32
|
||||
#include "windows.h"
|
||||
#include <io.h> /* _isatty */
|
||||
#else
|
||||
#include <dirent.h>
|
||||
#ifdef HAVE_UNISTD_H
|
||||
@@ -1509,3 +1510,11 @@ hts_boolean hts_rename_over_aside_selftest(httrackp *opt, const char *src,
|
||||
fconv(cdst, sizeof(cdst), dst);
|
||||
return rename_over_aside(opt, csrc, cdst);
|
||||
}
|
||||
|
||||
hts_boolean hts_stdout_isterminal(void) {
|
||||
#ifdef _WIN32
|
||||
return _isatty(_fileno(stdout)) ? HTS_TRUE : HTS_FALSE;
|
||||
#else
|
||||
return isatty(fileno(stdout)) ? HTS_TRUE : HTS_FALSE;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -149,6 +149,10 @@ hts_boolean hts_rename_over(httrackp *opt, const char *src, const char *dst);
|
||||
hts_boolean hts_rename_over_aside_selftest(httrackp *opt, const char *src,
|
||||
const char *dst);
|
||||
|
||||
/* True when stdout is a real terminal. Gate any VT escape or CR overwrite on
|
||||
it: a redirected log keeps that noise forever. */
|
||||
hts_boolean hts_stdout_isterminal(void);
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
@@ -55,7 +55,8 @@ Please visit our Website: http://www.httrack.com
|
||||
#include <openssl/rand.h>
|
||||
#endif
|
||||
|
||||
/* opt->state.warc value meaning "open failed once, do not retry". */
|
||||
/* opt->state.warc value meaning "no writer, and do not open one": the open
|
||||
failed, or the session is done with the archive. */
|
||||
#define WARC_DISABLED ((void *) ~(uintptr_t) 0)
|
||||
|
||||
/* Suffix of the in-progress archive when a previous one must survive it. */
|
||||
@@ -851,17 +852,39 @@ static void warc_cdx_flush(warc_writer *w) {
|
||||
FILE *f;
|
||||
char catbuff[CATBUFF_SIZE];
|
||||
size_t i;
|
||||
if (!w->cdx_on || w->cdx_path == NULL || w->cdx_count == 0)
|
||||
int werr, cerr;
|
||||
if (!w->cdx_on || w->cdx_path == NULL)
|
||||
return;
|
||||
if (w->cdx_count == 0) {
|
||||
/* Stale only if an index is still on disk and this run wrote an archive
|
||||
over what it describes: opened covers the swap and the in-place run. */
|
||||
if (w->opened && fsize_utf8(w->cdx_path) > 0)
|
||||
hts_log_print(w->opt, LOG_ERROR,
|
||||
"WARC: no record was indexed, %s was not rewritten",
|
||||
w->cdx_path);
|
||||
return;
|
||||
}
|
||||
qsort(w->cdx_lines, w->cdx_count, sizeof(char *), cdx_cmp);
|
||||
f = FOPEN(fconv(catbuff, sizeof(catbuff), w->cdx_path), "wb");
|
||||
if (f == NULL)
|
||||
if (f == NULL) {
|
||||
hts_log_print(w->opt, LOG_ERROR | LOG_ERRNO,
|
||||
"WARC: could not write the index %s", w->cdx_path);
|
||||
return;
|
||||
}
|
||||
for (i = 0; i < w->cdx_count; i++) {
|
||||
fputs(w->cdx_lines[i], f);
|
||||
fputc('\n', f);
|
||||
}
|
||||
fclose(f);
|
||||
werr = ferror(f) != 0;
|
||||
cerr = fclose(f) != 0;
|
||||
/* a write that already failed says more than fclose echoing it, and only
|
||||
fclose's errno is still fresh */
|
||||
if (werr)
|
||||
hts_log_print(w->opt, LOG_ERROR, "WARC: the index %s is incomplete",
|
||||
w->cdx_path);
|
||||
else if (cerr)
|
||||
hts_log_print(w->opt, LOG_ERROR | LOG_ERRNO,
|
||||
"WARC: could not write the index %s", w->cdx_path);
|
||||
}
|
||||
|
||||
/* ---- WACZ pages + packaging (--wacz) ---- */
|
||||
@@ -1669,7 +1692,19 @@ void warc_close_opt(httrackp *opt) {
|
||||
if (opt->state.warc != NULL && opt->state.warc != WARC_DISABLED) {
|
||||
warc_close((warc_writer *) opt->state.warc);
|
||||
}
|
||||
opt->state.warc = NULL;
|
||||
/* final: teardown still finalizes slots, and a reopen there would orphan a
|
||||
.tmp nobody closes (#1060) */
|
||||
opt->state.warc = WARC_DISABLED;
|
||||
}
|
||||
|
||||
void warc_abort_opt(httrackp *opt) {
|
||||
if (opt->state.warc != NULL && opt->state.warc != WARC_DISABLED) {
|
||||
warc_writer *const w = (warc_writer *) opt->state.warc;
|
||||
/* the session is rolling back, so this run must replace nothing */
|
||||
w->failed = HTS_TRUE;
|
||||
warc_close(w);
|
||||
}
|
||||
opt->state.warc = WARC_DISABLED;
|
||||
}
|
||||
|
||||
/* ---- one transaction ---- */
|
||||
|
||||
@@ -80,13 +80,19 @@ void warc_move_request(htsblk *src, htsblk *dst);
|
||||
void warc_adopt_rawspool(htsblk *r, const char *tmpfile_path);
|
||||
|
||||
/* Emit the request + response (or revisit) records for one finished
|
||||
transaction. Lazily opens the writer into opt->state.warc; a no-op (logged
|
||||
once) if the archive cannot be created. */
|
||||
transaction. Lazily opens the writer into opt->state.warc; a no-op if the
|
||||
archive cannot be created (logged once), and a silent one once the session
|
||||
has closed or abandoned it. */
|
||||
void warc_write_backtransaction(httrackp *opt, lien_back *back);
|
||||
|
||||
/* Close and free the writer held in opt->state.warc, if any. */
|
||||
/* Close and free the writer held in opt->state.warc, if any. Both this and
|
||||
warc_abort_opt() are final: a later transaction reopens nothing. */
|
||||
void warc_close_opt(httrackp *opt);
|
||||
|
||||
/* Same, but discards this run's archive: keeps the previous one and drops
|
||||
the temporaries. For a rolled-back session that transferred nothing. */
|
||||
void warc_abort_opt(httrackp *opt);
|
||||
|
||||
/* --- Direct writer API (used by the hooks above and the self-test). --- */
|
||||
|
||||
/* Create the archive at path (auto-named when path is WARC_AUTONAME), writing
|
||||
|
||||
47
src/htsweb.c
47
src/htsweb.c
@@ -67,6 +67,7 @@ Please visit our Website: http://www.httrack.com
|
||||
#include "htsurlport.h"
|
||||
#include "htsweb.h"
|
||||
#include "htscharset.h"
|
||||
#include "htsrandom.h"
|
||||
|
||||
#if USE_BEGINTHREAD==0
|
||||
#error fatal: no threads support
|
||||
@@ -101,50 +102,6 @@ static void htsweb_sig_brpipe(int code) {
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
/* RtlGenRandom, resolved at runtime so no import library is needed. */
|
||||
typedef BOOLEAN(WINAPI *hts_rtlgenrandom_t)(PVOID buffer, ULONG length);
|
||||
#endif
|
||||
|
||||
/* Fill buffer with system entropy; HTS_FALSE if no source answered. There is
|
||||
deliberately no weak fallback: the caller mints a secret with it. */
|
||||
static hts_boolean hts_random_bytes(void *buffer, size_t size) {
|
||||
#ifdef _WIN32
|
||||
hts_boolean ok = HTS_FALSE;
|
||||
HMODULE dll = LoadLibraryA("advapi32.dll");
|
||||
|
||||
if (dll != NULL) {
|
||||
hts_rtlgenrandom_t gen =
|
||||
(hts_rtlgenrandom_t) GetProcAddress(dll, "SystemFunction036");
|
||||
|
||||
/* the ULONG cast must not truncate; callers ask for a few dozen bytes */
|
||||
if (gen != NULL && size <= 0x10000 && gen(buffer, (ULONG) size)) {
|
||||
ok = HTS_TRUE;
|
||||
}
|
||||
FreeLibrary(dll);
|
||||
}
|
||||
return ok;
|
||||
#else
|
||||
unsigned char *dst = (unsigned char *) buffer;
|
||||
size_t done = 0;
|
||||
FILE *fp = fopen("/dev/urandom", "rb");
|
||||
|
||||
if (fp == NULL) {
|
||||
return HTS_FALSE;
|
||||
}
|
||||
while (done < size) {
|
||||
const size_t n = fread(dst + done, 1, size - done, fp);
|
||||
|
||||
if (n == 0) { /* short read is a hard failure, not partial credit */
|
||||
break;
|
||||
}
|
||||
done += n;
|
||||
}
|
||||
fclose(fp);
|
||||
return done == size ? HTS_TRUE : HTS_FALSE;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Threads that never return; no wait may count on them draining. */
|
||||
static int nonjoinable_threads = 0;
|
||||
|
||||
@@ -716,7 +673,7 @@ int __cdecl htsshow_loop(t_hts_callbackarg * carg, httrackp * opt, lien_back * b
|
||||
strcpybuff(StatsBuffer[index].state, "search");
|
||||
ok = 1;
|
||||
} else if (back[i].status == STATUS_FTP_TRANSFER) { // ohh le beau ftp
|
||||
char proto[] = "ftp";
|
||||
char proto[8] = "ftp";
|
||||
|
||||
if (back[i].url_adr[0]) {
|
||||
char *ep = strchr(back[i].url_adr, ':');
|
||||
|
||||
@@ -61,11 +61,6 @@ Please visit our Website: http://www.httrack.com
|
||||
assertf((*opt->filters.filptr) < opt->maxfilter); \
|
||||
} while (0)
|
||||
|
||||
typedef struct htspair_t {
|
||||
const char *tag;
|
||||
const char *attr;
|
||||
} htspair_t;
|
||||
|
||||
/* "embedded" */
|
||||
htspair_t hts_detect_embed[] = {
|
||||
{"img", "src"},
|
||||
@@ -133,11 +128,13 @@ int hts_acceptlink(httrackp * opt, int ptr,
|
||||
return forbidden_url;
|
||||
}
|
||||
|
||||
static int cmp_token(const char *tag, const char *cmp) {
|
||||
hts_boolean hts_cmp_tag_token(const char *tag, const char *cmp) {
|
||||
int p;
|
||||
|
||||
return (strncasecmp(tag, cmp, (p = (int) strlen(cmp))) == 0
|
||||
&& !isalnum((unsigned char) tag[p]));
|
||||
return (tag != NULL && strncasecmp(tag, cmp, (p = (int) strlen(cmp))) == 0 &&
|
||||
!isalnum((unsigned char) tag[p]))
|
||||
? HTS_TRUE
|
||||
: HTS_FALSE;
|
||||
}
|
||||
|
||||
/* TRUE if (tag, attribute) matches an embedded-asset pair in the table */
|
||||
@@ -145,7 +142,8 @@ static hts_boolean is_embed_pair(const htspair_t *table, const char *tag,
|
||||
const char *attribute) {
|
||||
int i;
|
||||
for (i = 0; table[i].tag != NULL; i++) {
|
||||
if (cmp_token(tag, table[i].tag) && cmp_token(attribute, table[i].attr))
|
||||
if (hts_cmp_tag_token(tag, table[i].tag) &&
|
||||
hts_cmp_tag_token(attribute, table[i].attr))
|
||||
return HTS_TRUE;
|
||||
}
|
||||
return HTS_FALSE;
|
||||
|
||||
@@ -56,6 +56,19 @@ hts_boolean hts_robots_forbids(httrackp *opt, const char *adr, const char *fil,
|
||||
hts_boolean filters_decided,
|
||||
hts_boolean filters_refused);
|
||||
|
||||
/* A (tag, attribute) pair naming a reference kind. */
|
||||
#ifndef HTS_DEF_DEFSTRUCT_htspair_t
|
||||
#define HTS_DEF_DEFSTRUCT_htspair_t
|
||||
|
||||
typedef struct htspair_t {
|
||||
const char *tag;
|
||||
const char *attr;
|
||||
} htspair_t;
|
||||
#endif
|
||||
|
||||
/* HTS_TRUE if tag starts with the whole token cmp; NULL tag never matches. */
|
||||
hts_boolean hts_cmp_tag_token(const char *tag, const char *cmp);
|
||||
|
||||
int hts_acceptlink(httrackp * opt, int ptr,
|
||||
const char *adr, const char *fil,
|
||||
const char *tag, const char *attribute,
|
||||
|
||||
@@ -73,6 +73,9 @@ static int linput(FILE * fp, char *s, int max);
|
||||
#ifdef HAVE_SYS_IOCTL_H
|
||||
#include <sys/ioctl.h>
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
#include <io.h> /* _isatty */
|
||||
#endif
|
||||
#include <ctype.h>
|
||||
/* END specific definitions */
|
||||
|
||||
@@ -190,6 +193,15 @@ static void vt_home(void) {
|
||||
static int term_cols = 80;
|
||||
static int term_rows = 24;
|
||||
|
||||
/* The library's hts_stdout_isterminal() is hidden by -fvisibility=hidden. */
|
||||
static hts_boolean stdout_isterminal(void) {
|
||||
#ifdef _WIN32
|
||||
return _isatty(_fileno(stdout)) ? HTS_TRUE : HTS_FALSE;
|
||||
#else
|
||||
return isatty(fileno(stdout)) ? HTS_TRUE : HTS_FALSE;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Returns HTS_TRUE if the terminal size changed since the last call. */
|
||||
static hts_boolean vt_size_refresh(void) {
|
||||
int cols = 0;
|
||||
@@ -327,7 +339,8 @@ static void __cdecl htsshow_uninit(t_hts_callbackarg * carg) {
|
||||
}
|
||||
static int __cdecl htsshow_start(t_hts_callbackarg * carg, httrackp * opt) {
|
||||
use_show = 0;
|
||||
if (opt->verbosedisplay == HTS_VERBOSE_FULL) {
|
||||
/* Cursor addressing needs a screen; the ENTER toggle re-enters here. */
|
||||
if (opt->verbosedisplay == HTS_VERBOSE_FULL && stdout_isterminal()) {
|
||||
use_show = 1;
|
||||
(void) vt_size_refresh();
|
||||
vt_clear();
|
||||
|
||||
@@ -126,10 +126,12 @@
|
||||
<ClCompile Include="htscmdline.c" />
|
||||
<ClCompile Include="htsurlport.c" />
|
||||
<ClCompile Include="htsmd5.c" />
|
||||
<ClCompile Include="htsnet.c" />
|
||||
<ClCompile Include="htsmodules.c" />
|
||||
<ClCompile Include="htsname.c" />
|
||||
<ClCompile Include="htsparse.c" />
|
||||
<ClCompile Include="htsproxy.c" />
|
||||
<ClCompile Include="htsrandom.c" />
|
||||
<ClCompile Include="htsrobots.c" />
|
||||
<ClCompile Include="htsselftest.c" />
|
||||
<ClCompile Include="htssinglefile.c" />
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
<ClCompile>
|
||||
<!-- Matches proxytrack_CFLAGS in Makefile.am. Standalone: it does not link
|
||||
libhttrack, it only borrows headers (hts_effective_mime is a macro). -->
|
||||
<PreprocessorDefinitions>WIN32;_CONSOLE;_MBCS;NO_MALLOCT;ZLIB_CONST;HTS_INTHASH_USES_MD5;ZLIB_DLL;WINVER=0x0601;_WIN32_WINNT=0x0601;_CRT_SECURE_NO_WARNINGS;_CRT_NONSTDC_NO_DEPRECATE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<PreprocessorDefinitions>WIN32;_CONSOLE;_MBCS;NO_MALLOCT;HTS_NO_LIBHTTRACK;ZLIB_CONST;HTS_INTHASH_USES_MD5;ZLIB_DLL;WINVER=0x0601;_WIN32_WINNT=0x0601;_CRT_SECURE_NO_WARNINGS;_CRT_NONSTDC_NO_DEPRECATE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<AdditionalIncludeDirectories>$(MSBuildThisFileDirectory);$(MSBuildThisFileDirectory)coucal;$(MSBuildThisFileDirectory)proxy;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||
@@ -100,6 +100,7 @@
|
||||
<ClCompile Include="proxy\proxytrack.c" />
|
||||
<ClCompile Include="proxy\store.c" />
|
||||
<ClCompile Include="htsurlport.c" />
|
||||
<ClCompile Include="htsnet.c" />
|
||||
<ClCompile Include="coucal\coucal.c" />
|
||||
<ClCompile Include="htsmd5.c" />
|
||||
<ClCompile Include="md5.c" />
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
#endif
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 3, 49, 17, 0
|
||||
PRODUCTVERSION 3, 49, 17, 0
|
||||
FILEVERSION 3, 49, 19, 0
|
||||
PRODUCTVERSION 3, 49, 19, 0
|
||||
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS VS_FF_DEBUG
|
||||
@@ -35,12 +35,12 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Xavier Roche"
|
||||
VALUE "FileDescription", VER_FILE_DESCRIPTION
|
||||
VALUE "FileVersion", "3.49.17"
|
||||
VALUE "FileVersion", "3.49.19"
|
||||
VALUE "InternalName", VER_ORIGINAL_FILENAME
|
||||
VALUE "LegalCopyright", "Copyright (C) 1998-2026 Xavier Roche and other contributors. GNU GPL v3 or later."
|
||||
VALUE "OriginalFilename", VER_ORIGINAL_FILENAME
|
||||
VALUE "ProductName", "HTTrack Website Copier"
|
||||
VALUE "ProductVersion", "3.49-17"
|
||||
VALUE "ProductVersion", "3.49-19"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -97,6 +97,7 @@
|
||||
<ItemGroup>
|
||||
<ClCompile Include="htsserver.c" />
|
||||
<ClCompile Include="htsweb.c" />
|
||||
<ClCompile Include="htsrandom.c" />
|
||||
<ClCompile Include="htscmdline.c" />
|
||||
<ClCompile Include="htsurlport.c" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -97,6 +97,10 @@ run_only "$tmp/q-bad" '"foo'
|
||||
refused "dangling-quote argument not refused cleanly"
|
||||
run_only "$tmp/q-lone" '"'
|
||||
refused "lone-quote argument not refused cleanly"
|
||||
# A dangling quote on an argument at the cap must not overflow the "Missing
|
||||
# quote in %s" panic buffer: the prefix plus a 1023-byte argument overran it.
|
||||
run_only "$tmp/q-long" "\"$(nchars 1022)"
|
||||
refused "near-cap dangling-quote argument overflowed the panic buffer"
|
||||
|
||||
# --pause (#185): valid MIN[:MAX] accepted; malformed, reversed, over-range and
|
||||
# non-finite values refused cleanly. NaN defeats naive `<`/`>` checks (it
|
||||
|
||||
@@ -1,8 +1,43 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Over-long FTP userinfo must fail the link, never log in under a clipped name
|
||||
# (#1032).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ftp_split_userpass bounds an over-long user:pass@ from a hostile ftp:// URL.
|
||||
out=$(httrack -O /dev/null -#test=ftp-userpass run)
|
||||
grep -q "ftp-userpass self-test OK" <<<"$out"
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/httrack_ftpuser.XXXXXX") || exit 1
|
||||
trap 'set +e; rm -rf "${tmpdir}"' EXIT
|
||||
trap 'exit 1' HUP INT QUIT TERM
|
||||
|
||||
out=$(httrack -O "${tmpdir}/st" "-#test=ftp-userpass" run 2>&1) ||
|
||||
fail "self-test exited non-zero: ${out}"
|
||||
grep -q "ftp-userpass self-test OK" <<<"${out}" || fail "unexpected output: ${out}"
|
||||
|
||||
# The reachable half: a URL carries the userinfo straight into user[256]/pass[256].
|
||||
rep() { awk -v n="$1" 'BEGIN { while (i++ < n) printf "a" }'; }
|
||||
|
||||
probe() {
|
||||
rm -rf "${tmpdir}/mir"
|
||||
httrack "ftp://$1@127.0.0.1:1/f.txt" -O "${tmpdir}/mir" -q >/dev/null 2>&1 ||
|
||||
fail "an FTP URL with ${#1}-byte userinfo crashed the engine"
|
||||
cat "${tmpdir}/mir/hts-log.txt"
|
||||
}
|
||||
|
||||
# The bare name has no ':' to bound it, only the '@', and is the worse branch.
|
||||
for u in "$(rep 256):pw" "u:$(rep 256)" "$(rep 256)"; do
|
||||
log=$(probe "${u}")
|
||||
grep -q "FTP user name or password too long" <<<"${log}" ||
|
||||
fail "over-long userinfo was not refused: ${log}"
|
||||
done
|
||||
|
||||
# 255 still fits, and a plain login is the outsider a widened gate would refuse.
|
||||
for u in "$(rep 255):pw" "u:$(rep 255)" "$(rep 255)" "bob:secret"; do
|
||||
log=$(probe "${u}")
|
||||
grep -q "Unable to connect to the server" <<<"${log}" ||
|
||||
fail "userinfo that fits did not reach the connect: ${log}"
|
||||
done
|
||||
|
||||
18
tests/01_zlib-warc-cdx-errors.test
Executable file
18
tests/01_zlib-warc-cdx-errors.test
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# An index that cannot be written, or that would be left empty next to a
|
||||
# rewritten archive, must say so; a first run with nothing to index must not.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
httrack_bin=$(cd "$(dirname "$(command -v httrack)")" && pwd)/httrack
|
||||
|
||||
scratch=$(mktemp -d)
|
||||
trap 'set +e; rm -rf "$scratch"' EXIT
|
||||
|
||||
out=$("$httrack_bin" -O /dev/null -#test=warc-cdx-errors "$scratch/")
|
||||
echo "$out"
|
||||
case "$out" in
|
||||
*": OK") ;;
|
||||
*) exit 1 ;;
|
||||
esac
|
||||
@@ -52,19 +52,7 @@ serverlog="${tmpdir}/server.out"
|
||||
"$python" "$server" --root "$(nativepath "$root")" \
|
||||
--mode-file "$(nativepath "$mode")" >"$serverlog" 2>&1 &
|
||||
serverpid=$!
|
||||
port=
|
||||
for _ in $(seq 1 300); do
|
||||
line=$(grep -m1 '^PORT ' "$serverlog" 2>/dev/null) && port="${line#PORT }" && break
|
||||
kill -0 "$serverpid" 2>/dev/null || {
|
||||
echo "ftp server exited early: $(cat "$serverlog")" >&2
|
||||
exit 1
|
||||
}
|
||||
sleep 0.1
|
||||
done
|
||||
test -n "$port" || {
|
||||
echo "could not discover ftp server port: $(cat "$serverlog")" >&2
|
||||
exit 1
|
||||
}
|
||||
port=$(discover_server_port "$serverlog" "$serverpid") || exit 1
|
||||
host="127.0.0.1_${port}"
|
||||
urls=()
|
||||
for name in keep empty stay; do
|
||||
|
||||
@@ -23,8 +23,11 @@ fail() {
|
||||
}
|
||||
|
||||
tmp=$(mktemp -d "${TMPDIR:-/tmp}/httrack_sutmo.XXXXXX") || exit 1
|
||||
trap 'set +e; rm -rf "$tmp"' EXIT
|
||||
trap 'rm -rf "$tmp"' HUP INT QUIT PIPE TERM
|
||||
# A colon-free root for the PATH shim below: MSYS hands out a drive-letter TMPDIR
|
||||
# and a PATH entry carrying a colon is read as two, which leaves it unreachable.
|
||||
shim=$(mktemp -d /tmp/httrack_sutmoshim.XXXXXX 2>/dev/null) || shim="$tmp/starve"
|
||||
trap 'set +e; rm -rf "$tmp" "$shim"' EXIT
|
||||
trap 'rm -rf "$tmp" "$shim"' HUP INT QUIT PIPE TERM
|
||||
out="$tmp/out"
|
||||
|
||||
# --- a hanging test is killed, named, and reported 124 ----------------------
|
||||
@@ -65,17 +68,20 @@ grep -q 'slow but healthy' "$out" || fail "healthy test output lost"
|
||||
|
||||
# The killed tree must really be gone, or the next test inherits its ports.
|
||||
sleep 1
|
||||
! grep -q "$tmp/90_wedged.test" <<<"$(ps -A -o args 2>/dev/null)" ||
|
||||
! grep -q "$tmp/90_wedged.test" <<<"$(ps_snapshot)" ||
|
||||
fail "the wedged test survived the guard"
|
||||
|
||||
# --- the budget is wall clock, not a count of poll iterations ----------------
|
||||
# Starve the poll and a counted budget stretches with it: 10 polls of a 0.1s tick
|
||||
# become 40s, so the guard fires long after the step it was meant to beat (#795).
|
||||
# HTTRACK_POLL_SLEEP forces the forked tick, the only one a shimmed sleep reaches;
|
||||
# the loop it stretches is the same one poll_wait's fd tick drives.
|
||||
(
|
||||
starve_sleep "$tmp/starve" 4 || fail "could not install the slow sleep"
|
||||
starve_sleep "$shim" 4 || fail "could not install the slow sleep"
|
||||
start=$SECONDS
|
||||
rc=0
|
||||
HTTRACK_TEST_TIMEOUT=1 bash "$driver" "$tmp/90_wedged.test" >"$out" 2>&1 || rc=$?
|
||||
HTTRACK_POLL_SLEEP=1 HTTRACK_TEST_TIMEOUT=1 \
|
||||
bash "$driver" "$tmp/90_wedged.test" >"$out" 2>&1 || rc=$?
|
||||
elapsed=$((SECONDS - start))
|
||||
test "$rc" -eq 124 || fail "starved guard reported $rc, want 124"
|
||||
# Generous: the diagnostics dump runs inside this window too.
|
||||
@@ -139,6 +145,142 @@ HTTRACK_TEST_TIMEOUT=600 bash "$driver" "$tmp/94_pacer.test" >"$out" 2>&1 || rc=
|
||||
test "$rc" -eq 0 || fail "a 60s step under a 600s budget reported $rc, want 0"
|
||||
grep -q 'the pacer let it through' "$out" || fail "the pacer skipped a test that fits"
|
||||
|
||||
# --- the process lists survive a host with no ps ----------------------------
|
||||
# Fedora's build root ships no procps, and the guard then named nothing (#1021).
|
||||
if ! is_windows && test -r /proc/self/stat; then
|
||||
mkdir -p "$tmp/nops"
|
||||
printf '#!/bin/sh\nexit 127\n' >"$tmp/nops/ps"
|
||||
chmod +x "$tmp/nops/ps"
|
||||
# A shell copy, not a renamed sleep: uutils refuses an argv[0] it does not own
|
||||
# (#1042). Either works, since both lists match on the basename alone.
|
||||
cp "${BASH:-$(command -v sh)}" "$tmp/httrack" || fail "no shell to copy"
|
||||
"$tmp/httrack" -c : || fail "cannot exec out of $tmp (noexec?)"
|
||||
# Read-write, so the open does not block and the timeout can bound it: the
|
||||
# fakes then fork nothing for the SIGABRT to orphan, and a test killed
|
||||
# outright leaks no process named httrack onto the runner.
|
||||
mkfifo "$tmp/hold"
|
||||
hold="read -t 60 _ <>'$tmp/hold'"
|
||||
# Started from a subshell so ppid and pgid differ: swapping those two columns
|
||||
# is invisible whenever the test shell happens to lead its own group.
|
||||
(
|
||||
"$tmp/httrack" -c "$hold" &
|
||||
echo $! >"$tmp/fake.pid"
|
||||
)
|
||||
# Its own group, to prove the filter keeps an outsider out.
|
||||
(
|
||||
set -m
|
||||
"$tmp/httrack" -c "$hold" &
|
||||
echo $! >"$tmp/outside.pid"
|
||||
)
|
||||
fake=$(cat "$tmp/fake.pid")
|
||||
outside=$(cat "$tmp/outside.pid")
|
||||
trap 'set +e; kill "$fake" "$outside" 2>/dev/null; rm -rf "$tmp"' EXIT
|
||||
# /proc holds the pid from the fork on, so a fake that never exec'd would fail
|
||||
# some later assertion instead of this one.
|
||||
running() {
|
||||
local i=0 argv0
|
||||
while test "$i" -lt 20; do
|
||||
{ read -r -d '' argv0 <"/proc/$1/cmdline"; } 2>/dev/null &&
|
||||
test "${argv0##*/}" = httrack && return 0
|
||||
sleep 0.1
|
||||
i=$((i + 1))
|
||||
done
|
||||
return 1
|
||||
}
|
||||
running "$fake" || fail "the fake engine never started"
|
||||
running "$outside" || fail "the outsider never started"
|
||||
# ppid and pgid of $1: the oracle the listing is checked against.
|
||||
procfields() { awk '{ sub(/.*\) /, ""); print $2, $3 }' "/proc/$1/stat"; }
|
||||
procstate() { awk '{ sub(/.*\) /, ""); print $1 }' "/proc/$1/stat" 2>/dev/null || echo gone; }
|
||||
read -r fppid pgid _ <<<"$(procfields "$fake")"
|
||||
read -r _ opgid _ <<<"$(procfields "$outside")"
|
||||
test "$fppid" != "$pgid" || fail "the fake shares ppid and pgid ($pgid)"
|
||||
test "$opgid" != "$pgid" || fail "the outsider landed in the group under test"
|
||||
(
|
||||
PATH="$tmp/nops:$PATH"
|
||||
! ps -A >/dev/null 2>&1 || fail "the ps shim did not take"
|
||||
snap=$(list_stray_processes "$pgid" group)
|
||||
# Line 1 is the header every consumer drops; a row there would be lost.
|
||||
case "$(head -n1 <<<"$snap")" in
|
||||
[0-9]*) fail "the no-ps listing has no header: $snap" ;;
|
||||
esac
|
||||
! grep -qE "^$outside " <<<"$snap" ||
|
||||
fail "no-ps group list reported pid $outside, in another group"
|
||||
read -r _ rppid rpgid _ <<<"$(grep -E "^$fake " <<<"$snap")"
|
||||
test "$rppid $rpgid" = "$fppid $pgid" ||
|
||||
fail "no-ps row for $fake reads ppid/pgid '$rppid $rpgid', want '$fppid $pgid'"
|
||||
engines=$(list_engine_pids "$pgid")
|
||||
grep -qx "$fake" <<<"$engines" || fail "no-ps engine list lost pid $fake"
|
||||
! grep -qx "$outside" <<<"$engines" ||
|
||||
fail "no-ps engine list reached outside the group (pid $outside)"
|
||||
# A platform with no stack mechanism must say so and leave the engine
|
||||
# alone. Shimmed, because no CI leg is one: the Hurd branch below runs
|
||||
# only on the buildds that failed for want of it.
|
||||
# shellcheck disable=SC2016 # the shim reads them, not us
|
||||
printf '#!/bin/sh\ncase "$1" in -s) echo GNU;; *) exec %s "$@";; esac\n' \
|
||||
"$(command -v uname)" >"$tmp/nops/uname"
|
||||
chmod +x "$tmp/nops/uname"
|
||||
grep -q "no stack mechanism known for GNU" <<<"$(request_engine_backtraces "$pgid")" ||
|
||||
fail "a platform with no stack mechanism reported nothing"
|
||||
case "$(procstate "$fake")" in
|
||||
Z | gone) fail "the no-mechanism branch signalled pid $fake" ;;
|
||||
esac
|
||||
rm -f "$tmp/nops/uname"
|
||||
|
||||
# The chain, not just its input: this is what reported nothing on Fedora.
|
||||
stacks=$(request_engine_backtraces "$pgid")
|
||||
! grep -q 'no engine process left' <<<"$stacks" ||
|
||||
fail "no-ps stack request found no engine to signal: $stacks"
|
||||
# Only Linux signals; elsewhere (Hurd's uname reports "GNU") the chain
|
||||
# owes the reader its no-mechanism report instead of silence.
|
||||
if test "$(uname -s)" = Linux; then
|
||||
# Gone, or a zombie: the fake is reparented, and a container's pid 1
|
||||
# does not always reap. kill -0 succeeds on a zombie, proving nothing.
|
||||
case "$(procstate "$fake")" in
|
||||
Z | gone) ;;
|
||||
*) fail "the SIGABRT never reached pid $fake" ;;
|
||||
esac
|
||||
else
|
||||
grep -q 'no stack mechanism known' <<<"$stacks" ||
|
||||
fail "no stack mechanism was reported on $(uname -s): $stacks"
|
||||
fi
|
||||
# With neither source the dump must say so, not print an empty section.
|
||||
# shellcheck disable=SC2317 # reached through ps_snapshot
|
||||
proc_snapshot() { return 1; }
|
||||
grep -q 'no process list' <<<"$(list_stray_processes "$pgid" group)" ||
|
||||
fail "a host with no ps and no /proc produced no notice"
|
||||
)
|
||||
kill "$fake" "$outside" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# --- an emulated buildd hides the engine behind its binfmt interpreter -------
|
||||
# Synthetic rows: no runner here has qemu-user, and the column shift is the point.
|
||||
if ! is_windows; then
|
||||
(
|
||||
# shellcheck disable=SC2317 # reached through the two matchers below
|
||||
ps_snapshot() {
|
||||
cat <<'EOF'
|
||||
PID PPID PGID ELAPSED S COMMAND
|
||||
11 1 40 12 S /usr/libexec/qemu-binfmt/hppa-binfmt-P /bld/src/httrack -q http://h/
|
||||
12 1 40 12 S /usr/bin/qemu-hppa-static /usr/bin/python3 /t/local-server.py 8080
|
||||
13 1 40 12 S /bld/src/httrack -q http://h/
|
||||
14 1 40 12 S /usr/bin/strace /bld/src/httrack -q http://h/
|
||||
15 1 40 12 S /usr/bin/qemu-img convert /srv/local-server.py out.raw
|
||||
16 1 40 12 S /usr/bin/qemu-nbd /mnt/httrack
|
||||
17 1 40 12 S /opt/my-custom-binfmt /mnt/httrack
|
||||
EOF
|
||||
}
|
||||
# Rows 14 to 17 are the controls: shifting past anything but an emulator
|
||||
# would take a wrapper, or a disk image, for the engine and kill it.
|
||||
engines=$(list_engine_pids 40)
|
||||
test "$engines" = $'11\n13' ||
|
||||
fail "emulated engine list is '$engines', want pids 11 and 13"
|
||||
named=$(list_stray_processes 0 named | awk 'NR > 1 { print $1 }')
|
||||
test "$named" = $'11\n12\n13' ||
|
||||
fail "emulated stray list is '$named', want pids 11, 12 and 13"
|
||||
)
|
||||
fi
|
||||
|
||||
# --- a wedged crawl yields a symbolized engine stack ------------------------
|
||||
# The whole point of the dump: name the frame the engine is stuck in. Windows has
|
||||
# neither half (MSYS signals do not reach a native httrack.exe, and that build has
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
bash check-network.sh || ! echo "skipping online unit tests" || exit 77
|
||||
testdir=$(cd "$(dirname "$0")" && pwd)
|
||||
|
||||
bash crawl-test.sh --errors 0 --files 5 httrack http://ut.httrack.com/simple/basic.html
|
||||
bash "$testdir/check-network.sh" || ! echo "skipping online unit tests" || exit 77
|
||||
|
||||
bash "$testdir/crawl-test.sh" --errors 0 --files 5 httrack http://ut.httrack.com/simple/basic.html
|
||||
|
||||
@@ -52,19 +52,7 @@ done
|
||||
serverlog="${tmpdir}/server.out"
|
||||
"$python" "$server" --root "$(nativepath "$root")" >"$serverlog" 2>&1 &
|
||||
serverpid=$!
|
||||
port=
|
||||
for _ in $(seq 1 300); do
|
||||
line=$(grep -m1 '^PORT ' "$serverlog" 2>/dev/null) && port="${line#PORT }" && break
|
||||
kill -0 "$serverpid" 2>/dev/null || {
|
||||
echo "ftp server exited early: $(cat "$serverlog")" >&2
|
||||
exit 1
|
||||
}
|
||||
sleep 0.1
|
||||
done
|
||||
test -n "$port" || {
|
||||
echo "could not discover ftp server port: $(cat "$serverlog")" >&2
|
||||
exit 1
|
||||
}
|
||||
port=$(discover_server_port "$serverlog" "$serverpid") || exit 1
|
||||
host="127.0.0.1_${port}"
|
||||
|
||||
urls=()
|
||||
|
||||
@@ -64,19 +64,7 @@ serverlog="${tmpdir}/server.out"
|
||||
--mode-file "$(nativepath "$mode")" --log "$(nativepath "$cmds")" \
|
||||
>"$serverlog" 2>&1 &
|
||||
serverpid=$!
|
||||
port=
|
||||
for _ in $(seq 1 300); do
|
||||
line=$(grep -m1 '^PORT ' "$serverlog" 2>/dev/null) && port="${line#PORT }" && break
|
||||
kill -0 "$serverpid" 2>/dev/null || {
|
||||
echo "ftp server exited early: $(cat "$serverlog")" >&2
|
||||
exit 1
|
||||
}
|
||||
sleep 0.1
|
||||
done
|
||||
test -n "$port" || {
|
||||
echo "could not discover ftp server port: $(cat "$serverlog")" >&2
|
||||
exit 1
|
||||
}
|
||||
port=$(discover_server_port "$serverlog" "$serverpid") || exit 1
|
||||
host="127.0.0.1_${port}"
|
||||
url="ftp://127.0.0.1:${port}/a.bin"
|
||||
mirror="${out}/${host}/a.bin"
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
bash check-network.sh || ! echo "skipping online unit tests" || exit 77
|
||||
testdir=$(cd "$(dirname "$0")" && pwd)
|
||||
|
||||
bash crawl-test.sh --errors 0 --files 3 \
|
||||
bash "$testdir/check-network.sh" || ! echo "skipping online unit tests" || exit 77
|
||||
|
||||
bash "$testdir/crawl-test.sh" --errors 0 --files 3 \
|
||||
--found ut.httrack.com/cookies/third.html \
|
||||
--found ut.httrack.com/cookies/second.html \
|
||||
--found ut.httrack.com/cookies/entrance.html \
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
bash check-network.sh || ! echo "skipping online unit tests" || exit 77
|
||||
testdir=$(cd "$(dirname "$0")" && pwd)
|
||||
|
||||
bash "$testdir/check-network.sh" || ! echo "skipping online unit tests" || exit 77
|
||||
|
||||
# unicode tests
|
||||
bash crawl-test.sh \
|
||||
bash "$testdir/crawl-test.sh" \
|
||||
--errors 1 --files 5 \
|
||||
--found 'café.ut.httrack.com/unicode-links/café3860.html' \
|
||||
--found 'café.ut.httrack.com/unicode-links/café30f4.html' \
|
||||
@@ -16,7 +18,7 @@ bash crawl-test.sh \
|
||||
'+*.ut.httrack.com/*' --robots=0
|
||||
|
||||
# unicode tests (bogus links)
|
||||
bash crawl-test.sh \
|
||||
bash "$testdir/crawl-test.sh" \
|
||||
--errors 0 --files 1 \
|
||||
--found 'ut.httrack.com/unicode-links/idna_bogus.html' \
|
||||
httrack 'http://ut.httrack.com/unicode-links/idna_bogus.html' \
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
bash check-network.sh || ! echo "skipping online unit tests" || exit 77
|
||||
testdir=$(cd "$(dirname "$0")" && pwd)
|
||||
|
||||
bash "$testdir/check-network.sh" || ! echo "skipping online unit tests" || exit 77
|
||||
|
||||
# unicode tests
|
||||
bash crawl-test.sh \
|
||||
bash "$testdir/crawl-test.sh" \
|
||||
--errors 1 --files 10 \
|
||||
--found ut.httrack.com/unicode-links/caf%a91bce.html \
|
||||
--found ut.httrack.com/unicode-links/café30f4.html \
|
||||
@@ -21,7 +23,7 @@ bash crawl-test.sh \
|
||||
--found ut.httrack.com/unicode-links/utf8.html \
|
||||
httrack http://ut.httrack.com/unicode-links/utf8.html
|
||||
|
||||
bash crawl-test.sh \
|
||||
bash "$testdir/crawl-test.sh" \
|
||||
--errors 2 --files 9 \
|
||||
--found ut.httrack.com/unicode-links/café3860.html \
|
||||
--found ut.httrack.com/unicode-links/café9fa8.html \
|
||||
@@ -36,7 +38,7 @@ bash crawl-test.sh \
|
||||
--found ut.httrack.com/unicode-links/default.html \
|
||||
httrack http://ut.httrack.com/unicode-links/default.html
|
||||
|
||||
bash crawl-test.sh \
|
||||
bash "$testdir/crawl-test.sh" \
|
||||
--errors 2 --files 9 \
|
||||
--found ut.httrack.com/unicode-links/caf%a9ae52.html \
|
||||
--found ut.httrack.com/unicode-links/caf%a9bf59.html \
|
||||
@@ -51,7 +53,7 @@ bash crawl-test.sh \
|
||||
--found ut.httrack.com/unicode-links/iso88591.html \
|
||||
httrack http://ut.httrack.com/unicode-links/iso88591.html
|
||||
|
||||
bash crawl-test.sh \
|
||||
bash "$testdir/crawl-test.sh" \
|
||||
--errors 4 --files 9 \
|
||||
--found ut.httrack.com/unicode-links/caf%a8%a6c72a.html \
|
||||
--found ut.httrack.com/unicode-links/caf%a9bf59.html \
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
bash check-network.sh || ! echo "skipping online unit tests" || exit 77
|
||||
testdir=$(cd "$(dirname "$0")" && pwd)
|
||||
|
||||
bash "$testdir/check-network.sh" || ! echo "skipping online unit tests" || exit 77
|
||||
|
||||
# http://code.google.com/p/httrack/issues/detail?id=42&can=1
|
||||
# we expect 2 errors only because other links are too longs (to be modified if suitable)
|
||||
bash crawl-test.sh --errors 2 --files 1 \
|
||||
bash "$testdir/crawl-test.sh" --errors 2 --files 1 \
|
||||
--found ut.httrack.com/overflow/longquerywithaccents.html \
|
||||
httrack http://ut.httrack.com/overflow/longquerywithaccents.php
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
bash check-network.sh || ! echo "skipping online unit tests" || exit 77
|
||||
testdir=$(cd "$(dirname "$0")" && pwd)
|
||||
|
||||
bash "$testdir/check-network.sh" || ! echo "skipping online unit tests" || exit 77
|
||||
|
||||
# http://code.google.com/p/httrack/issues/detail?id=4&can=1
|
||||
bash crawl-test.sh --errors 0 --files 4 \
|
||||
bash "$testdir/crawl-test.sh" --errors 0 --files 4 \
|
||||
--found ut.httrack.com/parsing/back5e1f.gif \
|
||||
--found ut.httrack.com/parsing/events.html \
|
||||
--found ut.httrack.com/parsing/fade230f4.gif \
|
||||
@@ -14,21 +16,21 @@ bash crawl-test.sh --errors 0 --files 4 \
|
||||
httrack http://ut.httrack.com/parsing/events.html
|
||||
|
||||
# http://code.google.com/p/httrack/issues/detail?id=2&can=1
|
||||
bash crawl-test.sh --errors 0 --files 3 \
|
||||
bash "$testdir/crawl-test.sh" --errors 0 --files 3 \
|
||||
--found ut.httrack.com/parsing/background-image.css \
|
||||
--found ut.httrack.com/parsing/background-image.html \
|
||||
--found ut.httrack.com/parsing/fade.gif \
|
||||
httrack http://ut.httrack.com/parsing/background-image.html
|
||||
|
||||
# javascript parsing
|
||||
bash crawl-test.sh --errors 0 --files 3 \
|
||||
bash "$testdir/crawl-test.sh" --errors 0 --files 3 \
|
||||
--found ut.httrack.com/parsing/back.gif \
|
||||
--found ut.httrack.com/parsing/fade.gif \
|
||||
--found ut.httrack.com/parsing/javascript.html \
|
||||
httrack http://ut.httrack.com/parsing/javascript.html
|
||||
|
||||
# handling of + before query string
|
||||
bash crawl-test.sh --errors 0 --files 6 \
|
||||
bash "$testdir/crawl-test.sh" --errors 0 --files 6 \
|
||||
--found ut.httrack.com/parsing/escaping.html \
|
||||
--found "ut.httrack.com/parsing/foo bar30f4.html" \
|
||||
--found "ut.httrack.com/parsing/foo bar5e1f.html" \
|
||||
@@ -39,7 +41,7 @@ bash crawl-test.sh --errors 0 --files 6 \
|
||||
|
||||
# handling of # encoded in filename
|
||||
# see http://code.google.com/p/httrack/issues/detail?id=25
|
||||
bash crawl-test.sh --errors 2 --files 4 \
|
||||
bash "$testdir/crawl-test.sh" --errors 2 --files 4 \
|
||||
--found "ut.httrack.com/parsing/escaping2.html" \
|
||||
--found "ut.httrack.com/parsing/++foo++bar++plus++.html" \
|
||||
--found "ut.httrack.com/parsing/foo#bar#.html" \
|
||||
|
||||
@@ -18,9 +18,10 @@ command -v curl >/dev/null 2>&1 || {
|
||||
echo "curl missing, skipping"
|
||||
exit 77
|
||||
}
|
||||
# MSYS cannot reap a native listener, and the orphan wedges the suite (#595).
|
||||
# A leak on a fully buffered stdout shows only through the pty below, and Windows
|
||||
# builds neither Python's pty module nor os.fork.
|
||||
if is_windows; then
|
||||
echo "windows: cannot reap a backgrounded proxytrack, skipping"
|
||||
echo "windows: python has no pty, so a buffered leak would be invisible, skipping"
|
||||
exit 77
|
||||
fi
|
||||
|
||||
|
||||
@@ -79,19 +79,7 @@ serverlog="${tmpdir}/server.out"
|
||||
--mode-file "$(nativepath "$mode")" --log "$(nativepath "$cmds")" \
|
||||
>"$serverlog" 2>&1 &
|
||||
serverpid=$!
|
||||
port=
|
||||
for _ in $(seq 1 300); do
|
||||
line=$(grep -m1 '^PORT ' "$serverlog" 2>/dev/null) && port="${line#PORT }" && break
|
||||
kill -0 "$serverpid" 2>/dev/null || {
|
||||
echo "ftp server exited early: $(cat "$serverlog")" >&2
|
||||
exit 1
|
||||
}
|
||||
sleep 0.1
|
||||
done
|
||||
test -n "$port" || {
|
||||
echo "could not discover ftp server port: $(cat "$serverlog")" >&2
|
||||
exit 1
|
||||
}
|
||||
port=$(discover_server_port "$serverlog" "$serverpid") || exit 1
|
||||
host="127.0.0.1_${port}"
|
||||
url="ftp://127.0.0.1:${port}/a.bin"
|
||||
mirror="${out}/${host}/a.bin"
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
bash check-network.sh || ! echo "skipping online unit tests" || exit 77
|
||||
testdir=$(cd "$(dirname "$0")" && pwd)
|
||||
|
||||
bash "$testdir/check-network.sh" || ! echo "skipping online unit tests" || exit 77
|
||||
|
||||
if test "${HTTPS_SUPPORT:-}" == "no"; then
|
||||
echo "no https support compiled, skipping"
|
||||
exit 77
|
||||
fi
|
||||
|
||||
bash crawl-test.sh --errors 0 --files 5 httrack https://ut.httrack.com/simple/basic.html
|
||||
bash "$testdir/crawl-test.sh" --errors 0 --files 5 httrack https://ut.httrack.com/simple/basic.html
|
||||
|
||||
@@ -18,11 +18,6 @@ command -v curl >/dev/null 2>&1 || {
|
||||
echo "curl missing, skipping"
|
||||
exit 77
|
||||
}
|
||||
# MSYS cannot reap a native listener, and the orphan wedges the suite (#595).
|
||||
if is_windows; then
|
||||
echo "windows: cannot reap a backgrounded proxytrack, skipping"
|
||||
exit 77
|
||||
fi
|
||||
|
||||
dir=$(mktemp -d)
|
||||
ptpid=
|
||||
@@ -49,8 +44,10 @@ alen=$(($(wc -c <"$dir/hdr") + $(wc -c <"$dir/body")))
|
||||
cat "$dir/hdr" "$dir/body"
|
||||
} >"$dir/in.arc"
|
||||
|
||||
# $() strips a trailing LF, not the CR Windows python's print can put before it.
|
||||
freeport() {
|
||||
"$python" -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close()'
|
||||
"$python" -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close()' |
|
||||
tr -d '\r'
|
||||
}
|
||||
proxyport=$(freeport)
|
||||
icpport=$(freeport)
|
||||
@@ -84,7 +81,7 @@ propfind() {
|
||||
|
||||
# Amplification, not raw length, clears the old 1024-byte reserve: 900 '&'
|
||||
# become 4500 bytes, written twice, all under the 1024-byte request-line cap.
|
||||
amps=$("$python" -c "print('&' * 900)")
|
||||
amps=$("$python" -c "print('&' * 900)" | tr -d '\r')
|
||||
resp=$(propfind "$amps") || {
|
||||
echo "FAIL: proxytrack died on an escapexml-amplified PROPFIND"
|
||||
cat "$dir/pt.log"
|
||||
@@ -104,7 +101,7 @@ test "$got" -eq 1800 || {
|
||||
}
|
||||
|
||||
# Unescaped, so the bound is shown to be on the written length, not on '&'.
|
||||
plain=$("$python" -c "print('a' * 900)")
|
||||
plain=$("$python" -c "print('a' * 900)" | tr -d '\r')
|
||||
resp=$(propfind "$plain") || {
|
||||
echo "FAIL: proxytrack died on a 900-byte plain PROPFIND path"
|
||||
cat "$dir/pt.log"
|
||||
|
||||
@@ -19,9 +19,11 @@ command -v curl >/dev/null 2>&1 || {
|
||||
echo "curl missing, skipping"
|
||||
exit 77
|
||||
}
|
||||
# MSYS cannot reap a native listener, and the orphan wedges the suite (#595).
|
||||
# A leak on a fully buffered stdout shows only through the pty below, and Windows
|
||||
# builds neither Python's pty module nor os.fork; proxytrack also skips the PID=
|
||||
# line, which request_log() cuts the banner off at.
|
||||
if is_windows; then
|
||||
echo "windows: cannot reap a backgrounded proxytrack, skipping"
|
||||
echo "windows: python has no pty, so a buffered leak would be invisible, skipping"
|
||||
exit 77
|
||||
fi
|
||||
|
||||
@@ -126,8 +128,13 @@ wait_drained() { # log
|
||||
|
||||
quietport=$(freeport)
|
||||
: >"$dir/quiet.log" # the drainer creates it asynchronously
|
||||
env -u HTS_LOG "$python" -c "$pty_exec" "$dir/quiet.log" \
|
||||
proxytrack "127.0.0.1:$quietport" "127.0.0.1:$(freeport)" "$dir/in.arc" &
|
||||
# Subshell, not env(1): uutils' env forks rather than execs, so stop_server would
|
||||
# kill the wrapper and leave proxytrack holding the pty open (#1042).
|
||||
(
|
||||
unset HTS_LOG
|
||||
exec "$python" -c "$pty_exec" "$dir/quiet.log" \
|
||||
proxytrack "127.0.0.1:$quietport" "127.0.0.1:$(freeport)" "$dir/in.arc"
|
||||
) &
|
||||
quietpid=$!
|
||||
wait_listen "$dir/quiet.log" "$quietpid"
|
||||
|
||||
|
||||
@@ -58,11 +58,13 @@ test "$(convert "$dir/legacy.arc" "$dir/c.arc")" = 1 ||
|
||||
# a third would put the record out of its reach again.
|
||||
hdrlen=$(head -1 "$dir/a.arc" | wc -c)
|
||||
declared=$(head -1 "$dir/a.arc" | awk '{print $NF}')
|
||||
sep=$(tail -c +$((hdrlen + declared + 1)) "$dir/a.arc" | head -c 3 | od -An -c | tr -d ' ')
|
||||
# od seeks for itself: piping the archive into `head -c` leaves the writer with
|
||||
# a closed pipe, and under pipefail that SIGPIPE is the test's exit status.
|
||||
sep=$(od -An -c -j $((hdrlen + declared)) -N 3 "$dir/a.arc" | tr -d ' ')
|
||||
test "$sep" = '\n\nh' || fail "version block ends on '$sep', expected two newlines"
|
||||
# The block itself must not end on one either: counting the blank line and then
|
||||
# writing a second one round-trips, but leaves the length a byte too long.
|
||||
inner=$(tail -c +$((hdrlen + declared - 1)) "$dir/a.arc" | head -c 2 | od -An -c | tr -d ' ')
|
||||
inner=$(od -An -c -j $((hdrlen + declared - 2)) -N 2 "$dir/a.arc" | tr -d ' ')
|
||||
test "$inner" != '\n\n' || fail "declared length still counts the blank line"
|
||||
|
||||
# An empty archive is not a corrupt one...
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Before the driver is sourced: with these set, anything it starts posts a commit
|
||||
# status on whatever the surrounding shell was last looking at.
|
||||
unset WATCHDOG_TOKEN WATCHDOG_REPO WATCHDOG_SHA WATCHDOG_URL
|
||||
|
||||
testdir=$(cd "$(dirname "$0")" && pwd)
|
||||
# shellcheck source=tests/ci-windows-suite.sh
|
||||
. "${testdir}/ci-windows-suite.sh"
|
||||
@@ -20,9 +24,15 @@ fail() {
|
||||
}
|
||||
|
||||
progress="$tmp/progress"
|
||||
kill_tree() { echo "KILL $1 at $(hb_now)"; }
|
||||
kill_tree() {
|
||||
hb_now
|
||||
echo "KILL $1 at $hb_time"
|
||||
}
|
||||
# Stubbed too, or the legs below would send a real SIGKILL to pid 4242.
|
||||
kill_pid() { echo "DIRECT $1 at $(hb_now)"; }
|
||||
kill_pid() {
|
||||
hb_now
|
||||
echo "DIRECT $1 at $hb_time"
|
||||
}
|
||||
# Twelve lines, against a bound of eight.
|
||||
list_stray_processes() {
|
||||
printf '%s\n' "$*" >"$tmp/strayargs"
|
||||
@@ -61,10 +71,21 @@ sleep() {
|
||||
printf 'RUN %ss%%a%%b.test\r at %ss\n' "$vnow" "$vnow" >>"$progress"
|
||||
echo "$vnow" >"$tmp/lastwrite"
|
||||
fi
|
||||
# One tick reports what a sleep that cannot exec reports. Unguarded, errexit
|
||||
# ends the watchdog here and every assertion below goes unanswered.
|
||||
test "$ticks" -ne 3 || return 126
|
||||
return 0
|
||||
}
|
||||
# A clock read through a command substitution runs one subshell deeper than the
|
||||
# watchdog itself, and that is a fork it must not need. Marker, not fail: an exit
|
||||
# here would only end that subshell. hb_depth is set at each call site below.
|
||||
hb_now() {
|
||||
test "$BASH_SUBSHELL" -eq "$hb_depth" || : >"$tmp/forked"
|
||||
hb_time=$vnow
|
||||
}
|
||||
hb_now() { echo "$vnow"; }
|
||||
|
||||
printf 'RUN 98_earlier.test at 0s\n' >"$progress"
|
||||
hb_depth=$BASH_SUBSHELL
|
||||
ci_suite_heartbeat 720 360 "$progress" 900 4242 >"$tmp/out" 2>&1
|
||||
|
||||
# Timed by the stub, not by the annotation. The kill is due one staticness window
|
||||
@@ -130,6 +151,7 @@ awk '/^::(notice|error) title=/ { if (prev != "") bad = 1 } { prev = $0 } END {
|
||||
vnow=9000
|
||||
ticks=0
|
||||
printf 'RUN 97_dead.test at 0s\n' >"$progress"
|
||||
hb_depth=$BASH_SUBSHELL
|
||||
ci_suite_heartbeat 960 360 "$progress" 900 4242 >"$tmp/late" 2>&1
|
||||
killed=$(sed -n 's/^KILL 4242 at \([0-9]*\)$/\1/p' "$tmp/late")
|
||||
test -n "$killed" || fail "a suite static from the start was never killed"
|
||||
@@ -138,7 +160,8 @@ test "$killed" -le 10080 || fail "killed at $killed, want 9960 within a tick"
|
||||
|
||||
# taskkill is a grandchild of its own target, so a leaves-first /T would reap the
|
||||
# watchdog before the root (#953): kill_tree here never returns, and the stubs
|
||||
# record the order, which the code under test cannot write to.
|
||||
# record the order, which the code under test cannot write to. The reporter goes
|
||||
# first: the kill runs no EXIT trap, and the tree walk reaches this heartbeat.
|
||||
rec="$tmp/killed"
|
||||
: >"$rec"
|
||||
vnow=9000
|
||||
@@ -146,18 +169,24 @@ ticks=0
|
||||
printf 'RUN 96_wedged.test at 0s\n' >"$progress"
|
||||
rc=0
|
||||
(
|
||||
watchdog=777
|
||||
kill_pid() { echo "DIRECT $1" >>"$rec"; }
|
||||
kill_tree() {
|
||||
echo "TREE $1" >>"$rec"
|
||||
exit 9
|
||||
}
|
||||
hb_depth=$BASH_SUBSHELL
|
||||
ci_suite_heartbeat 960 360 "$progress" 900 4242 >"$tmp/hedge" 2>&1
|
||||
) || rc=$?
|
||||
test "$rc" -eq 9 || fail "the tree kill never fired: watchdog returned $rc"
|
||||
test "$(sed -n 1p "$rec")" = "DIRECT 4242" ||
|
||||
test "$(sed -n 1p "$rec")" = "DIRECT 777" ||
|
||||
fail "the reporter was not killed ahead of the suite: $(tr '\n' '/' <"$rec")"
|
||||
test "$(sed -n 2p "$rec")" = "DIRECT 4242" ||
|
||||
fail "the target was not signalled directly ahead of the tree walk: $(tr '\n' '/' <"$rec")"
|
||||
test "$(sed -n 2p "$rec")" = "TREE 4242" ||
|
||||
test "$(sed -n 3p "$rec")" = "TREE 4242" ||
|
||||
fail "the tree was not killed after the direct signal: $(tr '\n' '/' <"$rec")"
|
||||
test "$(sed -n '$=' "$rec")" -eq 2 || fail "extra kills: $(tr '\n' '/' <"$rec")"
|
||||
test "$(sed -n '$=' "$rec")" -eq 3 || fail "extra kills: $(tr '\n' '/' <"$rec")"
|
||||
|
||||
test ! -e "$tmp/forked" || fail "the clock was read through a subshell, a fork a starved box cannot spare"
|
||||
|
||||
echo "heartbeat OK"
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Before the driver is sourced: with these set, anything it starts posts a commit
|
||||
# status on whatever the surrounding shell was last looking at.
|
||||
unset WATCHDOG_TOKEN WATCHDOG_REPO WATCHDOG_SHA WATCHDOG_URL
|
||||
|
||||
testdir=$(cd "$(dirname "$0")" && pwd)
|
||||
driver="$testdir/ci-windows-suite.sh"
|
||||
|
||||
@@ -39,36 +43,109 @@ grep -q usage <<<"$out" || fail "argless run said: $out"
|
||||
# sibling's.
|
||||
run=$tmp/run
|
||||
mkdir -p "$run"
|
||||
cp "$driver" "$testdir/testlib.sh" "$testdir/test-timeout.sh" "$run/"
|
||||
cp "$driver" "$testdir/testlib.sh" "$testdir/test-timeout.sh" \
|
||||
"$testdir/ci-windows-watchdog.ps1" "$run/"
|
||||
chmod u+w "$run"/*.sh # distcheck's srcdir is read-only, and cp carries that over
|
||||
cat >>"$run/testlib.sh" <<'EOF'
|
||||
reap_leftover_processes() { return 0; }
|
||||
EOF
|
||||
# Records who sourced this copy, so assert_staged can tell the staged driver from
|
||||
# the srcdir one. test-timeout.sh sources it too, hence the caller and not a bare
|
||||
# marker file.
|
||||
cat >>"$run/testlib.sh" <<EOF
|
||||
printf '%s\n' "\${BASH_SOURCE[1]##*/}" >>"$tmp/sourced-by"
|
||||
EOF
|
||||
: >"$tmp/sourced-by"
|
||||
|
||||
# The suite under test has to be the staged copy: only it sources the testlib.sh
|
||||
# neutered above, and the srcdir one would reap sibling engines under "make check -j".
|
||||
assert_staged() {
|
||||
grep -qx 'ci-windows-suite.sh' "$tmp/sourced-by" ||
|
||||
fail "the srcdir driver ran, not the staged copy: $(sort -u "$tmp/sourced-by" | tr '\n' ' ')"
|
||||
: >"$tmp/sourced-by"
|
||||
}
|
||||
|
||||
bin=$tmp/bin
|
||||
mkdir -p "$bin"
|
||||
printf '#!/bin/sh\nexit 0\n' >"$bin/httrack"
|
||||
# shellcheck disable=SC2016 # $2 is the stub's own argument
|
||||
printf '#!/bin/sh\necho "$2"\n' >"$bin/cygpath"
|
||||
# The first child the driver forks: the token has to be gone before it runs.
|
||||
# shellcheck disable=SC2016 # $2 and the token are the stub's own expansions
|
||||
printf '#!/bin/sh\necho "TOKEN=${WATCHDOG_TOKEN:-}" >>%s\necho "$2"\n' "$tmp/cygpathtoken" \
|
||||
>"$bin/cygpath"
|
||||
chmod +x "$bin/httrack" "$bin/cygpath"
|
||||
test -x "$bin/httrack" || {
|
||||
# The driver puts this bindir first on PATH, so this is the interpreter it finds.
|
||||
# Unconditional, because a real PowerShell here runs the real watchdog, and only
|
||||
# the scrub above stands between that and api.github.com.
|
||||
wdargv=$tmp/wdargv
|
||||
wdmark=$tmp/wdmark
|
||||
: >"$wdargv"
|
||||
cat >"$bin/pwsh" <<EOF
|
||||
#!/bin/sh
|
||||
{ echo "TOKEN=\${WATCHDOG_TOKEN:-}"; printf '%s\n' "\$@"; } >>"$wdargv"
|
||||
echo "watchdog ready"
|
||||
# Ticks rather than sleeps: a pid can be reused, a running reporter cannot.
|
||||
n=0
|
||||
while :; do
|
||||
n=\$((n + 1))
|
||||
echo "\$n" >"$wdmark.part"
|
||||
mv "$wdmark.part" "$wdmark"
|
||||
sleep 0.2
|
||||
done
|
||||
EOF
|
||||
chmod +x "$bin/pwsh"
|
||||
# Ahead of any command -v, which asks access(X_OK) and so reads false here.
|
||||
test -x "$bin/pwsh" || {
|
||||
echo "SKIP: ${TMPDIR:-/tmp} is noexec, the stub bindir cannot be run"
|
||||
exit 77
|
||||
}
|
||||
test "$(PATH="$bin:$PATH" command -v pwsh)" = "$bin/pwsh" ||
|
||||
fail "the stub does not shadow PowerShell: $(PATH="$bin:$PATH" command -v pwsh)"
|
||||
|
||||
# The stub advances $wdmark every 0.2s, so a marker that moves says it still runs.
|
||||
# Sampled three times, or a loaded box starving the stub would read as a kill.
|
||||
still_ticking() {
|
||||
local a b
|
||||
a=$(cat "$wdmark" 2>/dev/null || true)
|
||||
for _ in 1 2 3; do
|
||||
sleep 1
|
||||
b=$(cat "$wdmark" 2>/dev/null || true)
|
||||
test -n "$b" && test "$a" != "$b" && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Not still_ticking negated: the marker's mv outlives the kill -9 of its parent,
|
||||
# and one differing sample would read that straggler as a live reporter (#1058).
|
||||
reporter_stopped() {
|
||||
local a b _try
|
||||
for _try in 1 2 3 4 5; do
|
||||
a=$(cat "$wdmark" 2>/dev/null || true)
|
||||
sleep 3
|
||||
b=$(cat "$wdmark" 2>/dev/null || true)
|
||||
test -n "$b" && test "$a" = "$b" && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# One per glob the driver enumerates: an empty category is counted as a failing
|
||||
# test named after the unexpanded pattern, which would drown the accounting.
|
||||
for t in 00_runnable 10_engine-pass 13_zlib-pass 14_local-pass 15_watchdog-pass \
|
||||
for t in 00_runnable 13_zlib-pass 14_local-pass 15_watchdog-pass \
|
||||
16_crawl_proxy_https 17_crawl-log-salvage; do
|
||||
printf '#!/bin/sh\nexit 0\n' >"$run/$t.test"
|
||||
done
|
||||
# One test reports back what it inherited: the token the step is handed can post
|
||||
# commit statuses, and a forged one has already reached a commit under test.
|
||||
# shellcheck disable=SC2016 # the stub expands it, not this shell
|
||||
printf '#!/bin/sh\necho "TOKEN=${WATCHDOG_TOKEN:-}" >%s\nexit 0\n' "$tmp/childtoken" \
|
||||
>"$run/10_engine-pass.test"
|
||||
printf '#!/bin/sh\nexit 77\n' >"$run/11_engine-skip.test"
|
||||
printf '#!/bin/sh\nexit 3\n' >"$run/12_engine-fail.test"
|
||||
|
||||
cd "$run"
|
||||
rc=0
|
||||
out=$(RUNNER_TEMP="$tmp" GITHUB_STEP_SUMMARY="$tmp/summary" \
|
||||
out=$(RUNNER_TEMP="$tmp" GITHUB_STEP_SUMMARY="$tmp/summary" WATCHDOG_TOKEN=s3cr3t \
|
||||
bash ./ci-windows-suite.sh "$bin" 2>&1) || rc=$?
|
||||
assert_staged
|
||||
# The skip set and the pass floor are pinned to the real suite, so a stub run
|
||||
# ends on the floor; what is under test is the tally that reaches it.
|
||||
test "$rc" -eq 1 || fail "stub suite exited $rc, want 1 from the pass floor"
|
||||
@@ -77,6 +154,20 @@ grep -q '^ran=9 pass=7 fail=1 skip=1$' <<<"$out" ||
|
||||
grep -q '::error::only 7 tests passed (1 skipped)' <<<"$out" ||
|
||||
fail "the pass floor did not report the count"
|
||||
grep -q 'FAIL 12_engine-fail.test (exit 3)' <<<"$out" || fail "the failing test was not named"
|
||||
|
||||
# This launches the watchdog the way the real driver does, not through a test
|
||||
# sourcing the helper, proving it starts and holds a token nothing else sees.
|
||||
grep -q '^watchdog ready$' watchdog.log || fail "no watchdog was launched: $(cat watchdog.log)"
|
||||
test -r "$tmp/childtoken" || fail "the test that reads its environment did not run"
|
||||
grep -qx 'TOKEN=' "$tmp/childtoken" ||
|
||||
fail "a test inherited the status token: $(cat "$tmp/childtoken")"
|
||||
grep -qx 'TOKEN=s3cr3t' "$wdargv" ||
|
||||
fail "the watchdog was launched without its token: $(cat "$wdargv")"
|
||||
test -s "$tmp/cygpathtoken" || fail "the cygpath stub never ran"
|
||||
grep -q 'TOKEN=s3cr3t' "$tmp/cygpathtoken" &&
|
||||
fail "the driver forked a child before scrubbing its token: $(cat "$tmp/cygpathtoken")"
|
||||
# An orphan runs out its own deadline, re-posting a frozen tail past the step (#795).
|
||||
reporter_stopped || fail "the driver exited leaving its reporter running"
|
||||
# Positive control for the gate below: with every category matched it stays silent.
|
||||
grep -q 'matched no tests' <<<"$out" && fail "a full suite reported an empty category"
|
||||
|
||||
@@ -92,6 +183,7 @@ rm -f "$run/13_zlib-pass.test"
|
||||
rc=0
|
||||
out=$(RUNNER_TEMP="$tmp" GITHUB_STEP_SUMMARY="$tmp/summary" \
|
||||
bash ./ci-windows-suite.sh "$bin" 2>&1) || rc=$?
|
||||
assert_staged
|
||||
test "$rc" -eq 1 || fail "an empty category exited $rc, want 1"
|
||||
grep -q '::error::test category zlib matched no tests' <<<"$out" ||
|
||||
fail "the empty category was not named: $out"
|
||||
@@ -105,9 +197,40 @@ rm -f "$run/00_runnable.test"
|
||||
rc=0
|
||||
out=$(RUNNER_TEMP="$tmp" GITHUB_STEP_SUMMARY="$tmp/summary" \
|
||||
bash ./ci-windows-suite.sh "$bin" 2>&1) || rc=$?
|
||||
assert_staged
|
||||
test "$rc" -eq 1 || fail "an empty single-test category exited $rc, want 1"
|
||||
grep -q '::error::test category runnable matched no tests' <<<"$out" ||
|
||||
fail "the empty single-test category was not named: $out"
|
||||
grep -q 'FAIL 00_runnable' <<<"$out" && fail "the unexpanded name was run as a test: $out"
|
||||
|
||||
# The wedge branch has to end the reporter itself: its kill of the suite is a
|
||||
# TerminateProcess, which runs no EXIT trap. Forked the way the driver forks it,
|
||||
# since the branch reads $watchdog from the shell it was forked from.
|
||||
wedge_leg() (
|
||||
PATH="$bin:$PATH"
|
||||
export PATH
|
||||
prog=$run/wedge-progress.log
|
||||
printf 'RUN 99_wedged.test at 0s\n' >"$prog"
|
||||
ci_watchdog_pid=''
|
||||
ci_start_native_watchdog "$prog" || fail "the wedge leg launched no watchdog"
|
||||
watchdog=$ci_watchdog_pid
|
||||
still_ticking || fail "the wedge leg's reporter was not running to begin with"
|
||||
# Its own process group, so the tree kill below cannot reach this shell.
|
||||
set -m
|
||||
sleep 300 &
|
||||
victim=$!
|
||||
set +m
|
||||
ci_suite_heartbeat 0 1 "$prog" 0 "$victim" >"$tmp/wedge.out" 2>&1 &
|
||||
wait "$!"
|
||||
grep -q '::error title=suite watchdog::' "$tmp/wedge.out" ||
|
||||
fail "the wedge branch was never reached: $(cat "$tmp/wedge.out")"
|
||||
reporter_stopped || fail "the wedge kill orphaned the reporter"
|
||||
)
|
||||
# Held back: bash announces every job it reaps from a signal, which reads as a
|
||||
# failure in a log that passed.
|
||||
wedge_leg >"$tmp/wedge.log" 2>&1 || {
|
||||
cat "$tmp/wedge.log" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "ci-windows driver OK"
|
||||
|
||||
@@ -63,11 +63,24 @@ grep -q "^Caught signal 11$" <<<"$worker" || fail "-#c=threadstack: no 'Caught s
|
||||
# worker's, which is what had no alternate stack.
|
||||
worker_frames=$(frame_count "$worker")
|
||||
plain_frames=$(frame_count "$plain")
|
||||
test "$worker_frames" -ge 100 ||
|
||||
fail "-#c=threadstack: $worker_frames frames, expected a runaway recursion"
|
||||
test "$plain_frames" -lt 100 ||
|
||||
fail "-#c=segv: $plain_frames frames, the threshold no longer discriminates"
|
||||
|
||||
# Naming no frame at all is the unwinder failing outright, not a weak one, and
|
||||
# the floor below would take it for loong64. hts_print_backtrace() says which.
|
||||
if grep -q "No stack trace available" <<<"$worker"; then
|
||||
fail "-#c=threadstack: the report carries no stack trace at all"
|
||||
fi
|
||||
|
||||
# armhf and loong64 cannot unwind past the frame that faulted on the guard page
|
||||
# (180 skips on the same floor); the release trace below still judges the fix.
|
||||
if [ "$worker_frames" -ge 1 ] && [ "$worker_frames" -lt 6 ]; then
|
||||
echo "-#c=threadstack: $worker_frames frames, too few for this unwinder to" \
|
||||
"show the recursion" >&2
|
||||
elif [ "$worker_frames" -lt 100 ]; then
|
||||
fail "-#c=threadstack: $worker_frames frames, expected a runaway recursion"
|
||||
fi
|
||||
|
||||
# Everything above only proves the stack gets installed: the crashing worker
|
||||
# never returns, so the release hook never runs there. -#test=threadwait spawns
|
||||
# workers that do return, and the syscall trace says what each did with its
|
||||
|
||||
@@ -88,32 +88,36 @@ env MAKEFLAGS= MFLAGS= "$make" -C "$abs_top_builddir/src" install-DevIncludesDAT
|
||||
}
|
||||
|
||||
headers=("$tmp/include/httrack"/*.h)
|
||||
[ "${#headers[@]}" -ge 10 ] || fail "only ${#headers[@]} headers installed, the list cannot be right"
|
||||
[ -f "$tmp/include/httrack/htssafe.h" ] || fail "htssafe.h was not installed"
|
||||
|
||||
# No libc hands getnameinfo() to a strict-ISO unit, so a consumer of the two
|
||||
# socket headers must ask for POSIX. Prefer 2001: 2008 also un-hides strnlen,
|
||||
# which would stop those two covering that half of #972.
|
||||
posix_argv=(-D_POSIX_C_SOURCE=200112L)
|
||||
printf '#include <httrack/htsnet.h>\n' >"$tmp/tu.c"
|
||||
if ! "${cc_argv[@]}" "${cpp_argv[@]}" "${posix_argv[@]}" "-std=${stds[0]}" \
|
||||
-fsyntax-only "$tmp/tu.c" 2>/dev/null; then
|
||||
fail "htsnet.h needs POSIX.1-2008, which un-hides strnlen and would leave that half of #972 uncovered"
|
||||
fi
|
||||
# Derived from DevIncludes_DATA so a new header needs no edit here. A header
|
||||
# dropped from that list moves both sides, hence the named checks below.
|
||||
declared=$(awk '/^DevIncludes_DATA[[:space:]]*=/ { inlist = 1 }
|
||||
inlist {
|
||||
last = ($0 !~ /\\$/)
|
||||
sub(/^[^=]*=/, "")
|
||||
gsub(/\\/, " ")
|
||||
for (i = 1; i <= NF; i++)
|
||||
if ($i ~ /\.h$/) n++
|
||||
if (last) exit
|
||||
}
|
||||
END { print n + 0 }' "${abs_top_srcdir:?}/src/Makefile.am")
|
||||
[ "$declared" -ge 10 ] || fail "read only $declared headers out of DevIncludes_DATA, the parse is wrong"
|
||||
[ "${#headers[@]}" -eq "$declared" ] ||
|
||||
fail "${#headers[@]} headers installed, DevIncludes_DATA declares $declared"
|
||||
for h in htssafe.h htsnet.h htsopt.h; do
|
||||
[ -f "$tmp/include/httrack/$h" ] || fail "$h was not installed"
|
||||
done
|
||||
|
||||
# No -D_POSIX_C_SOURCE anywhere below: every installed header, htsnet.h and
|
||||
# htsopt.h included, must compile against the strict-ISO libc alone (#1001).
|
||||
count=0
|
||||
bad=0
|
||||
for h in "${headers[@]}"; do
|
||||
b=$(basename "$h")
|
||||
argv=("${cpp_argv[@]}")
|
||||
case "$b" in
|
||||
htsnet.h | htsopt.h) argv+=("${posix_argv[@]}") ;;
|
||||
esac
|
||||
printf '#include <httrack/%s>\n' "$b" >"$tmp/tu.c"
|
||||
for std in "${stds[@]}"; do
|
||||
for mode in -UHTS_INTERNAL_BYTECODE -DHTS_INTERNAL_BYTECODE; do
|
||||
count=$((count + 1))
|
||||
if ! "${cc_argv[@]}" "${argv[@]}" "-std=$std" "$mode" -fsyntax-only "$tmp/tu.c" 2>"$tmp/cc.log"; then
|
||||
if ! "${cc_argv[@]}" "${cpp_argv[@]}" "-std=$std" "$mode" -fsyntax-only "$tmp/tu.c" 2>"$tmp/cc.log"; then
|
||||
echo "$b does not compile under -std=$std ($mode):" >&2
|
||||
head -5 "$tmp/cc.log" >&2
|
||||
bad=1
|
||||
@@ -122,6 +126,7 @@ for h in "${headers[@]}"; do
|
||||
done
|
||||
done
|
||||
echo "compiled ${#headers[@]} headers x ${#stds[@]} std x 2 bytecode modes = $count units" >&2
|
||||
[ "$count" -eq $((${#headers[@]} * ${#stds[@]} * 2)) ] || fail "the compile loop skipped units"
|
||||
[ "$bad" -eq 0 ] || fail "installed headers do not compile in strict-ISO mode"
|
||||
|
||||
# A macro body is only compiled where it expands, so the include-only loop above
|
||||
@@ -178,19 +183,23 @@ int main(void) {
|
||||
return 0;
|
||||
}
|
||||
EOF
|
||||
# Vacuous if the gate handed us libc's strnlen. Match that branch, not the
|
||||
# loop, or a mutated loop reads as a lost fallback instead. Needs no runnable
|
||||
# binary, so it stays outside the gate below.
|
||||
for std in "${stds[@]}"; do
|
||||
pp=$("${cc_argv[@]}" "${cpp_argv[@]}" "-std=$std" -E "$tmp/strnlen.c") ||
|
||||
fail "cannot preprocess the htssafe_strnlen_ probe under -std=$std"
|
||||
if grep -q 'return strnlen' <<<"$pp"; then
|
||||
fail "-std=$std left htssafe_strnlen_ on libc, the differential proves nothing"
|
||||
fi
|
||||
done
|
||||
|
||||
# A noexec TMPDIR or a cross-compiler would red the run for the wrong reason.
|
||||
printf 'int main(void) { return 0; }\n' >"$tmp/tu.c"
|
||||
if ! ("${cc_argv[@]}" "${cpp_argv[@]}" -o "$tmp/tu" "$tmp/tu.c" 2>/dev/null && "$tmp/tu"); then
|
||||
echo "cannot run a binary built in $tmp, skipping the htssafe_strnlen_ differential" >&2
|
||||
else
|
||||
for std in "${stds[@]}"; do
|
||||
# Vacuous if the gate handed us libc's strnlen. Match that branch, not
|
||||
# the loop, or a mutated loop reads as a lost fallback instead.
|
||||
pp=$("${cc_argv[@]}" "${cpp_argv[@]}" "-std=$std" -E "$tmp/strnlen.c") ||
|
||||
fail "cannot preprocess the htssafe_strnlen_ probe under -std=$std"
|
||||
if grep -q 'return strnlen' <<<"$pp"; then
|
||||
fail "-std=$std left htssafe_strnlen_ on libc, the check below proves nothing"
|
||||
fi
|
||||
"${cc_argv[@]}" "${cpp_argv[@]}" "-std=$std" -o "$tmp/strnlen" "$tmp/strnlen.c" 2>"$tmp/cc.log" || {
|
||||
head -5 "$tmp/cc.log" >&2
|
||||
fail "the htssafe_strnlen_ probe does not build under -std=$std"
|
||||
@@ -221,9 +230,6 @@ else
|
||||
for h in "${headers[@]}"; do
|
||||
b=$(basename "$h")
|
||||
argv=("${base_cpp_argv[@]}" -Wall -Wextra -Werror)
|
||||
case "$b" in
|
||||
htsnet.h | htsopt.h) argv+=("${posix_argv[@]}") ;;
|
||||
esac
|
||||
printf '#include <httrack/%s>\nint main() { return 0; }\n' "$b" >"$tmp/tu.cpp"
|
||||
for std in "${cxx_stds[@]}"; do
|
||||
for mode in -UHTS_INTERNAL_BYTECODE -DHTS_INTERNAL_BYTECODE; do
|
||||
|
||||
@@ -6,14 +6,34 @@
|
||||
set -euo pipefail
|
||||
|
||||
srcdir="${abs_top_srcdir:?not run under make check}"
|
||||
metainfo="${srcdir}/html/server/div/com.httrack.WebHTTrack.metainfo.xml"
|
||||
|
||||
command -v appstreamcli >/dev/null 2>&1 || {
|
||||
echo "appstreamcli not installed; skipping" >&2
|
||||
# --no-net/--nonet: validate the file, not the reachability of the screenshot
|
||||
# host. Fedora's build gates on appstream-util, stricter about icons and
|
||||
# metadata than appstreamcli.
|
||||
validate() {
|
||||
case $1 in
|
||||
appstreamcli) appstreamcli validate --no-net --explain "$2" ;;
|
||||
appstream-util) appstream-util validate-relax --nonet "$2" ;;
|
||||
*)
|
||||
echo "FAIL: no validator named $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
tools=()
|
||||
for tool in appstreamcli appstream-util; do
|
||||
command -v "${tool}" >/dev/null 2>&1 && tools+=("${tool}")
|
||||
done
|
||||
test ${#tools[@]} -gt 0 || {
|
||||
echo "no appstream validator installed; skipping" >&2
|
||||
exit 77
|
||||
}
|
||||
test -f "${metainfo}" || {
|
||||
echo "FAIL: ${metainfo} not found" >&2
|
||||
|
||||
# html/Makefile.am installs these by glob; a second one must not ship unchecked.
|
||||
metainfos=("${srcdir}"/html/server/div/*.metainfo.xml)
|
||||
test -f "${metainfos[0]}" || {
|
||||
echo "FAIL: no metainfo under ${srcdir}/html/server/div" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -23,21 +43,18 @@ work=$(mktemp -d "${TMPDIR:-/tmp}/appstream.XXXXXX") || {
|
||||
}
|
||||
trap 'set +e; rm -rf "${work}"' EXIT
|
||||
|
||||
# --no-net: validate the file, not the reachability of the screenshot host.
|
||||
validate() { appstreamcli validate --no-net --explain "$1"; }
|
||||
|
||||
validate "${metainfo}" || {
|
||||
echo "FAIL: metainfo does not validate" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Positive control: an appstreamcli that never fails would pass the above.
|
||||
grep -v '<id>com.httrack.WebHTTrack</id>' "${metainfo}" >"${work}/noid.xml"
|
||||
if validate "${work}/noid.xml" >"${work}/noid.log" 2>&1; then
|
||||
echo "FAIL: metainfo without <id> validated; the check proves nothing" >&2
|
||||
exit 1
|
||||
fi
|
||||
grep -q component-id-missing "${work}/noid.log" ||
|
||||
echo "note: id-less copy failed for another reason: $(tail -3 "${work}/noid.log")" >&2
|
||||
|
||||
echo "appstream metainfo: valid"
|
||||
for metainfo in "${metainfos[@]}"; do
|
||||
# Positive control: a validator that never fails would pass the check below.
|
||||
sed '/<id>/d' "${metainfo}" >"${work}/noid.xml"
|
||||
for tool in "${tools[@]}"; do
|
||||
validate "${tool}" "${metainfo}" || {
|
||||
echo "FAIL: ${metainfo##*/} does not validate under ${tool}" >&2
|
||||
exit 1
|
||||
}
|
||||
if validate "${tool}" "${work}/noid.xml" >"${work}/noid.log" 2>&1; then
|
||||
echo "FAIL: ${tool} validated a metainfo with no <id>; it proves nothing" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "appstream metainfo: ${metainfo##*/} valid (${tools[*]})"
|
||||
done
|
||||
|
||||
42
tests/224_engine-ftp-cmdlen.test
Executable file
42
tests/224_engine-ftp-cmdlen.test
Executable file
@@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# A too-long FTP command must be refused, never truncated or aborted (#1019).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/httrack_ftpcmdlen.XXXXXX") || exit 1
|
||||
trap 'set +e; rm -rf "${tmpdir}"' EXIT
|
||||
trap 'exit 1' HUP INT QUIT TERM
|
||||
|
||||
out=$(httrack -O "${tmpdir}/st" "-#test=ftp-cmdlen" run 2>&1) ||
|
||||
fail "self-test exited non-zero: ${out}"
|
||||
grep -q "ftp-cmdlen self-test OK" <<<"${out}" || fail "unexpected output: ${out}"
|
||||
|
||||
# The reachable half: the copy into _adr[256] aborted on a long host.
|
||||
rep() { awk -v n="$1" 'BEGIN { while (i++ < n) printf "a" }'; }
|
||||
|
||||
probe_host() {
|
||||
rm -rf "${tmpdir}/host"
|
||||
httrack "ftp://$1/f.txt" -O "${tmpdir}/host" -q >/dev/null 2>&1 ||
|
||||
fail "a ${#1}-byte FTP host name crashed the engine"
|
||||
cat "${tmpdir}/host/hts-log.txt"
|
||||
}
|
||||
|
||||
for n in 256 257 300; do
|
||||
log=$(probe_host "$(rep "${n}")")
|
||||
grep -q "Host name too long" <<<"${log}" ||
|
||||
fail "a ${n}-byte host was not rejected: ${log}"
|
||||
done
|
||||
|
||||
# 255 still fits _adr[256]; 127.0.0.1:1 is the outsider a widened gate swallows.
|
||||
log=$(probe_host "$(rep 255)")
|
||||
grep -q "Unable to get server's address" <<<"${log}" ||
|
||||
fail "a 255-byte host did not reach the resolver: ${log}"
|
||||
log=$(probe_host "127.0.0.1:1")
|
||||
grep -q "Unable to connect to the server" <<<"${log}" ||
|
||||
fail "a normal host did not reach the connect: ${log}"
|
||||
99
tests/225_install-manifest.test
Executable file
99
tests/225_install-manifest.test
Executable file
@@ -0,0 +1,99 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Distro packages (Fedora %files, debian/*.files) name every installed file, so a
|
||||
# file appearing or vanishing here breaks their build and not ours; it broke
|
||||
# Fedora's twice in three weeks.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
: "${abs_top_builddir:?not run under make check}"
|
||||
manifest="${abs_top_srcdir:?not run under make check}/tests/install-manifest.txt"
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# macOS names the library .dylib, and Windows has no distro packaging to protect.
|
||||
test "$(uname -s)" = Linux || {
|
||||
echo "install layout is pinned on Linux only; skipping" >&2
|
||||
exit 77
|
||||
}
|
||||
test -r "${manifest}" || fail "no ${manifest}"
|
||||
|
||||
# The docs are dropped below, so a docdir holding datadir would drop the pinned
|
||||
# data files with them and leave nothing to compare.
|
||||
docdir=${CONFIGURED_DOCDIR:?not run under make check}
|
||||
case "${CONFIGURED_DATADIR:?not run under make check}" in
|
||||
"${docdir%/}" | "${docdir%/}"/*)
|
||||
echo "docdir contains datadir; skipping" >&2
|
||||
exit 77
|
||||
;;
|
||||
esac
|
||||
|
||||
work=$(mktemp -d "${TMPDIR:-/tmp}/manifest.XXXXXX") || {
|
||||
echo "no tmpdir" >&2
|
||||
exit 1
|
||||
}
|
||||
trap 'set +e; rm -rf "${work}"' EXIT
|
||||
|
||||
stage=${work}/stage
|
||||
env -u MAKEFLAGS -u MAKELEVEL "${MAKE:-make}" -C "${abs_top_builddir}" \
|
||||
install DESTDIR="${stage}" >"${work}/install.log" 2>&1 || {
|
||||
cat "${work}/install.log" >&2
|
||||
fail "make install DESTDIR=${stage}"
|
||||
}
|
||||
|
||||
# Fold the configured directories back to tokens so the pin survives any --prefix
|
||||
# or --libdir. Longest path first, so a nested directory beats the one it sits in
|
||||
# whatever the layout puts where.
|
||||
: >"${work}/dirs"
|
||||
for var in DOCDIR HTMLDIR MANDIR INCLUDEDIR LIBDIR BINDIR DATADIR; do
|
||||
name=CONFIGURED_${var}
|
||||
path=${!name:-}
|
||||
test -n "${path}" || fail "${name} not set"
|
||||
path=${path%/}
|
||||
printf '%s\t%s\t%s\n' "${#path}" "${path}" "${var}" >>"${work}/dirs"
|
||||
done
|
||||
LC_ALL=C sort -rn "${work}/dirs" -o "${work}/dirs"
|
||||
|
||||
paths=() tokens=()
|
||||
while IFS=$'\t' read -r _ path var; do
|
||||
paths+=("${path}")
|
||||
tokens+=("${var}")
|
||||
done <"${work}/dirs"
|
||||
|
||||
# Matched literally: a directory holding a regex metacharacter would silently
|
||||
# fold nothing if this were a pattern.
|
||||
fold() {
|
||||
local line=$1 i
|
||||
for i in "${!paths[@]}"; do
|
||||
case ${line} in
|
||||
"${paths[i]}"/*)
|
||||
printf '@%s@/%s\n' "${tokens[i]}" "${line#"${paths[i]}"/}"
|
||||
return
|
||||
;;
|
||||
esac
|
||||
done
|
||||
printf '%s\n' "${line}"
|
||||
}
|
||||
|
||||
# Dropped: the docs, since where they sit under docdir is a --htmldir choice each
|
||||
# distro makes its own way and no package names them one by one; the .a/.la
|
||||
# libtool bookkeeping no distro ships; and the library revision, which moves
|
||||
# every release. The soname digit stays: a bump renames Debian's package.
|
||||
(cd "${stage}" && find . \( -type f -o -type l \) | sed 's|^\.||') |
|
||||
while IFS= read -r line; do fold "${line}"; done |
|
||||
sed -e '/^@DOCDIR@\//d' -e '/^@HTMLDIR@\//d' \
|
||||
-e '/\.la$/d' -e '/\.a$/d' \
|
||||
-e 's|\(\.so\.[0-9][0-9]*\)\.[0-9][0-9]*\.[0-9][0-9]*$|\1.@revision@|' |
|
||||
LC_ALL=C sort >"${work}/actual"
|
||||
|
||||
# An install that staged nothing matches nothing; say that, not a 300-line diff.
|
||||
grep -qx '@BINDIR@/httrack' "${work}/actual" || fail "nothing staged under ${stage}"
|
||||
|
||||
LC_ALL=C sort "${manifest}" >"${work}/expected"
|
||||
diff -u "${work}/expected" "${work}/actual" ||
|
||||
fail "the installed file set changed; update tests/install-manifest.txt and say so in the release notes -- Fedora's %files and debian/*.files name these files"
|
||||
|
||||
echo "install manifest: $(wc -l <"${work}/actual") files"
|
||||
726
tests/226_watchdog-native.test
Normal file
726
tests/226_watchdog-native.test
Normal file
@@ -0,0 +1,726 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# The off-box suite watchdog (#795): its pure decisions through -SelfTest, what
|
||||
# reaches the status API, the process APIs it must never name, and the driver
|
||||
# wiring no .ps1 can check for itself.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Before the driver is sourced: with these set, anything it starts posts a commit
|
||||
# status on whatever the surrounding shell was last looking at.
|
||||
unset WATCHDOG_TOKEN WATCHDOG_REPO WATCHDOG_SHA WATCHDOG_URL
|
||||
export WATCHDOG_CONTEXT='226_watchdog-native (never posted)'
|
||||
|
||||
testdir=$(cd "$(dirname "$0")" && pwd)
|
||||
# shellcheck source=tests/testlib.sh
|
||||
. "$testdir/testlib.sh"
|
||||
|
||||
top=${abs_top_srcdir:-$(cd "$testdir/.." && pwd)}
|
||||
wdscript="$testdir/ci-windows-watchdog.ps1"
|
||||
driver="$testdir/ci-windows-suite.sh"
|
||||
workflow="$top/.github/workflows/windows-build.yml"
|
||||
|
||||
tmp=$(mktemp -d "${TMPDIR:-/tmp}/httrack_wdnat.XXXXXX")
|
||||
sinkpidfile="$tmp/sink.pid"
|
||||
: >"$sinkpidfile"
|
||||
toppid=$$
|
||||
trap 'set +e; stop_server "$(cat "$sinkpidfile" 2>/dev/null)"; rm -rf "$tmp"' EXIT
|
||||
trap 'exit 1' TERM
|
||||
# Signalled, since a bare exit only ends the subshell a leg runs in.
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
kill -TERM "$toppid" 2>/dev/null || true
|
||||
exit 1
|
||||
}
|
||||
|
||||
test -r "$wdscript" || fail "no $wdscript"
|
||||
test -z "${WATCHDOG_TOKEN:-}${WATCHDOG_SHA:-}" || fail "a credential survived the scrub"
|
||||
|
||||
# The prose in the script names the very calls the audits forbid. A trailing
|
||||
# comment goes only on a quote-free line: cutting at any '#' would hide a call
|
||||
# sitting after an issue number inside a string.
|
||||
uncommented() { sed -e 's/^[[:space:]]*#.*$//' -e "/['\"]/! s/[[:space:]]#.*$//" "$1"; }
|
||||
|
||||
# Telemetry only, so every spawn and kill idiom is out, aliases and short forms
|
||||
# included: PowerShell needs neither the System. prefix nor a space after '&'.
|
||||
forbidden_calls() {
|
||||
uncommented "$1" | grep -nEi \
|
||||
'Start-Process|Start-Job|Start-ThreadJob|(Start|Stop)-Service|Stop-Process|Invoke-Expression|Invoke-Item|Invoke-Command|Invoke-(Cim|Wmi)Method|Get-WmiObject|Register-Scheduled(Job|Task)|WScript\.Shell|taskkill|cmd\.exe|wmic|schtasks|Diagnostics\.Process|\.Kill\(|(^|[^-a-zA-Z0-9_])(iex|saps|spps|spjb|sajb|spsv|sasv|start|kill)([^-a-zA-Z0-9_]|$)|(^|[^-a-zA-Z0-9_])&[^-a-zA-Z0-9_=&]'
|
||||
}
|
||||
if hits=$(forbidden_calls "$wdscript"); then
|
||||
fail "the watchdog names a process API: $hits"
|
||||
fi
|
||||
# One control per idiom: a denylist is worth only the entries that actually fire.
|
||||
# shellcheck disable=SC2016 # PowerShell source, not this shell's expansions
|
||||
for probe in 'Start-Process notepad.exe' 'Stop-Process -Id 4 -Force' \
|
||||
'[Diagnostics.Process]::Start("x")' '(Get-Process -Name httrack).Kill()' \
|
||||
'Invoke-Expression $cmd' '& "C:\\x.exe" -y' 'Start-Job { 1 }' 'taskkill /F /IM x.exe' \
|
||||
'iex $cmd' '$cmd | iex' '&"C:\\x.exe"' '&$exe' 'saps notepad.exe' 'start notepad.exe' \
|
||||
'spps -Name httrack' 'spjb { 1 }' 'sajb { 1 }' 'kill -Name httrack' \
|
||||
'Start-Service w32time' 'Stop-Service w32time' 'spsv w32time' 'sasv w32time' \
|
||||
'Get-WmiObject Win32_Process' \
|
||||
'Invoke-CimMethod -ClassName Win32_Process -MethodName Create' \
|
||||
'Invoke-WmiMethod -Class Win32_Process -Name Create' \
|
||||
'Invoke-Command -ScriptBlock { 1 }' 'Register-ScheduledJob -Name x -ScriptBlock { 1 }' \
|
||||
'Register-ScheduledTask -TaskName x -Action $a' \
|
||||
'(New-Object -ComObject WScript.Shell).Run("x")' \
|
||||
'Write-Host "#795"; Start-Process x'; do
|
||||
{
|
||||
cat "$wdscript"
|
||||
echo "$probe"
|
||||
} >"$tmp/probe.ps1"
|
||||
if ! forbidden_calls "$tmp/probe.ps1" >/dev/null; then
|
||||
fail "the process-API audit cannot see: $probe"
|
||||
fi
|
||||
done
|
||||
|
||||
# shellcheck disable=SC2016 # $env: is PowerShell's, not this shell's
|
||||
grep -q '\$env:WATCHDOG_TOKEN' "$wdscript" ||
|
||||
fail "the watchdog does not read its token from the environment"
|
||||
if sed -n '/^param(/,/^)/p' "$wdscript" | grep -qi 'token'; then
|
||||
fail "the watchdog takes its token as a parameter, where the process list exposes it"
|
||||
fi
|
||||
grep -q "ApiBase = 'https://api.github.com'" "$wdscript" ||
|
||||
fail "the watchdog's default endpoint is not the GitHub API"
|
||||
# One state, so nothing downstream can read a verdict out of a status that only
|
||||
# carries telemetry. The job's own conclusion is the verdict.
|
||||
test "$(grep -c "state = 'success'" "$wdscript")" -eq 1 ||
|
||||
fail "the watchdog posts something other than the one telemetry state"
|
||||
# The token stays in the driver's own shell: exported, every test it runs would
|
||||
# inherit a credential that can post statuses. 172 drives that end to end.
|
||||
grep -q '^unset WATCHDOG_TOKEN$' "$driver" ||
|
||||
fail "the driver leaves its token in the environment its tests inherit"
|
||||
# A background holder of the step's stdout keeps the step open past the suite (#949).
|
||||
grep -q '>>watchdog.log 2>&1' "$driver" ||
|
||||
fail "the watchdog's output is not redirected off the step's stdout"
|
||||
|
||||
# Sourcing the driver is enough to start an interpreter that can post, so every
|
||||
# test that does it has to scrub first.
|
||||
scrubs() {
|
||||
grep -q 'ci-windows-suite\.sh' "$1" || return 0
|
||||
grep -qx 'unset WATCHDOG_TOKEN WATCHDOG_REPO WATCHDOG_SHA WATCHDOG_URL' "$1"
|
||||
}
|
||||
for t in "$testdir"/*.test; do
|
||||
scrubs "$t" || fail "$(basename "$t") can reach the driver without scrubbing its credentials"
|
||||
done
|
||||
printf '#!/bin/sh\n. ./ci-windows-suite.sh\n' >"$tmp/unscrubbed.test"
|
||||
scrubs "$tmp/unscrubbed.test" && fail "the scrub audit passes a test that does not scrub"
|
||||
|
||||
# PATH holds MSYS-style entries only, and TMPDIR on Windows is a D:/... path
|
||||
# that would split it on the drive colon.
|
||||
posixtmp=$tmp
|
||||
if is_windows && command -v cygpath >/dev/null 2>&1; then
|
||||
posixtmp=$(cygpath -u "$tmp")
|
||||
fi
|
||||
bin="$posixtmp/bin"
|
||||
mkdir -p "$bin"
|
||||
# Renamed into place, so the poll below cannot read a half-written file.
|
||||
printf '#!/bin/sh\nprintf "%%s\\n" "$@" >%s/argv.part\nmv %s/argv.part %s/argv\necho "watchdog ready"\n' \
|
||||
"$posixtmp" "$posixtmp" "$posixtmp" >"$bin/argvstub"
|
||||
printf '#!/bin/sh\nexit 127\n' >"$bin/deadstub"
|
||||
chmod +x "$bin/argvstub" "$bin/deadstub"
|
||||
usestub() { cp "$bin/$1" "$bin/pwsh" && cp "$bin/$1" "$bin/powershell.exe"; }
|
||||
usestub argvstub
|
||||
|
||||
if test -x "$bin/pwsh"; then
|
||||
(
|
||||
cd "$posixtmp"
|
||||
# shellcheck disable=SC2030 # local to this subshell is the point
|
||||
PATH="$bin:$PATH"
|
||||
export PATH
|
||||
# An unshadowed pwsh would drive the real watchdog, and every assertion
|
||||
# below would still read as a plain launch failure.
|
||||
test "$(command -v pwsh)" = "$bin/pwsh" ||
|
||||
fail "the stub does not shadow PowerShell: $(command -v pwsh)"
|
||||
# shellcheck source=tests/ci-windows-suite.sh
|
||||
. "$driver"
|
||||
ci_watchdog_pid=''
|
||||
ci_start_native_watchdog "$posixtmp/progress.log" ||
|
||||
fail "the launch reported no watchdog with a PowerShell on PATH"
|
||||
test -n "$ci_watchdog_pid" || fail "the launch set no pid"
|
||||
# wait only reaps a child, which is what the teardown has to signal.
|
||||
wait "$ci_watchdog_pid" || fail "the watchdog is not a child of the driver"
|
||||
|
||||
# A pid comes from the fork, so a dead interpreter still yields one.
|
||||
usestub deadstub
|
||||
ci_watchdog_pid='dangling'
|
||||
rc=0
|
||||
ci_start_native_watchdog "$posixtmp/progress.log" || rc=$?
|
||||
test "$rc" -ne 0 || fail "an interpreter exiting 127 was reported as launched"
|
||||
test -z "$ci_watchdog_pid" || fail "a failed launch left pid $ci_watchdog_pid behind"
|
||||
)
|
||||
for _ in $(seq 1 100); do
|
||||
test -r "$posixtmp/argv" && break
|
||||
sleep 0.1
|
||||
done
|
||||
test -r "$posixtmp/argv" || fail "the watchdog was never launched"
|
||||
grep -qx -- '-SelfTest' "$posixtmp/argv" && fail "the production launch runs the self-test"
|
||||
# Bypass is load-bearing: Windows PowerShell refuses an unsigned .ps1 without it.
|
||||
for flag in -NoProfile -NonInteractive -ExecutionPolicy Bypass -File; do
|
||||
grep -qx -- "$flag" "$posixtmp/argv" ||
|
||||
fail "the launch dropped $flag: $(tr '\n' ' ' <"$posixtmp/argv")"
|
||||
done
|
||||
grep -qx -- '-ApiBase' "$posixtmp/argv" &&
|
||||
fail "the production launch redirects the status API away from GitHub"
|
||||
grep -qx -- '-NoPost' "$posixtmp/argv" &&
|
||||
fail "the production launch cannot post at all"
|
||||
# Native form, because a non-MSYS interpreter cannot resolve a /d/a/... path.
|
||||
grep -qx -- "$(nativepath "$wdscript")" "$posixtmp/argv" ||
|
||||
fail "the launch did not name the script: $(tr '\n' ' ' <"$posixtmp/argv")"
|
||||
grep -qx -- "$(nativepath "$posixtmp/progress.log")" "$posixtmp/argv" ||
|
||||
fail "the launch did not pass the progress log"
|
||||
else
|
||||
echo "note: $tmp is noexec, the driver wiring was not driven"
|
||||
fi
|
||||
|
||||
py=$(find_python || true)
|
||||
if ! test -r "$workflow"; then
|
||||
test -d "$top/.github" && fail "$workflow is gone but $top/.github is not"
|
||||
echo "note: no .github under $top (dist tarball), the workflow audits did not run"
|
||||
elif test -z "$py" || ! "$py" -c 'import yaml' 2>/dev/null; then
|
||||
echo "note: no python3 with PyYAML here, the workflow audits did not run"
|
||||
else
|
||||
# Parsed, not grepped: every key below has to be read from the block that
|
||||
# owns it, and an ordinary reformatting of the workflow must not move it.
|
||||
cat >"$tmp/wf.py" <<'PY'
|
||||
import sys
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
def steps_of(wf):
|
||||
return [s for j in (wf.get("jobs") or {}).values() for s in (j.get("steps") or [])]
|
||||
|
||||
|
||||
def checkouts(wf):
|
||||
return [s for s in steps_of(wf) if str(s.get("uses", "")).startswith("actions/checkout")]
|
||||
|
||||
|
||||
def suite_steps(wf):
|
||||
return [s for s in steps_of(wf) if "ci-windows-suite.sh" in str(s.get("run", ""))]
|
||||
|
||||
|
||||
def suite_jobs(wf):
|
||||
return [
|
||||
j
|
||||
for j in (wf.get("jobs") or {}).values()
|
||||
if any("ci-windows-suite.sh" in str(s.get("run", "")) for s in (j.get("steps") or []))
|
||||
]
|
||||
|
||||
|
||||
# A job-level block replaces the workflow's outright, so one naming only contents
|
||||
# silently drops statuses: write.
|
||||
def perms_of(wf, job):
|
||||
p = job.get("permissions", wf.get("permissions"))
|
||||
if p in ("read-all", "write-all"):
|
||||
w = "read" if p == "read-all" else "write"
|
||||
return {"contents": w, "statuses": w}
|
||||
return p if isinstance(p, dict) else {}
|
||||
|
||||
|
||||
WANT_ENV = {
|
||||
"WATCHDOG_TOKEN": "${{ secrets.GITHUB_TOKEN }}",
|
||||
"WATCHDOG_REPO": "${{ github.repository }}",
|
||||
# The merge commit, which no PR checks UI reads.
|
||||
"WATCHDOG_SHA": "${{ github.sha }}",
|
||||
}
|
||||
|
||||
|
||||
def audit(wf):
|
||||
bad = []
|
||||
if not checkouts(wf):
|
||||
bad.append("no actions/checkout step to audit")
|
||||
for s in checkouts(wf):
|
||||
if (s.get("with") or {}).get("persist-credentials") is not False:
|
||||
bad.append("a checkout persists the job token for the whole job")
|
||||
jobs, found = suite_jobs(wf), suite_steps(wf)
|
||||
if len(jobs) != 1 or len(found) != 1:
|
||||
bad.append("%d jobs and %d steps run the suite, cannot tell which is watched" % (len(jobs), len(found)))
|
||||
return bad
|
||||
perms = perms_of(wf, jobs[0])
|
||||
if perms.get("statuses") != "write":
|
||||
bad.append("the suite job cannot post a commit status: statuses is %r" % perms.get("statuses"))
|
||||
if perms.get("contents") != "read":
|
||||
bad.append("contents: read was dropped from the suite job")
|
||||
env = found[0].get("env") or {}
|
||||
for v, want in WANT_ENV.items():
|
||||
if env.get(v) != want:
|
||||
bad.append("%s is %r, want %r" % (v, env.get(v), want))
|
||||
# Both axes, or the matrix legs overwrite each other's status.
|
||||
for axis in ("matrix.platform", "matrix.configuration"):
|
||||
if axis not in str(env.get("WATCHDOG_CONTEXT") or ""):
|
||||
bad.append("WATCHDOG_CONTEXT does not name %s" % axis)
|
||||
if "github.run_id" not in str(env.get("WATCHDOG_URL") or ""):
|
||||
bad.append("WATCHDOG_URL does not link the run being watched")
|
||||
cap = found[0].get("timeout-minutes")
|
||||
if not isinstance(cap, int):
|
||||
bad.append("the suite step has no timeout-minutes of its own")
|
||||
else:
|
||||
print("timeout=%d" % cap)
|
||||
return bad
|
||||
|
||||
|
||||
def mutate(wf, kind):
|
||||
if kind == "perm":
|
||||
wf["permissions"]["statuses"] = "read"
|
||||
elif kind == "jobperm":
|
||||
suite_jobs(wf)[0]["permissions"] = {"contents": "read"}
|
||||
elif kind == "creds":
|
||||
for s in checkouts(wf):
|
||||
(s.get("with") or {}).pop("persist-credentials", None)
|
||||
elif kind == "env":
|
||||
suite_steps(wf)[0]["env"].pop("WATCHDOG_TOKEN")
|
||||
elif kind == "token":
|
||||
suite_steps(wf)[0]["env"]["WATCHDOG_TOKEN"] = "${{ secrets.WATCHDOG_PAT }}"
|
||||
elif kind == "sha":
|
||||
suite_steps(wf)[0]["env"]["WATCHDOG_SHA"] = "${{ github.event.pull_request.head.sha }}"
|
||||
elif kind == "context":
|
||||
suite_steps(wf)[0]["env"]["WATCHDOG_CONTEXT"] = "windows-suite"
|
||||
elif kind == "url":
|
||||
suite_steps(wf)[0]["env"]["WATCHDOG_URL"] = "${{ github.server_url }}"
|
||||
elif kind == "cap":
|
||||
suite_steps(wf)[0].pop("timeout-minutes")
|
||||
elif kind != "none":
|
||||
raise SystemExit("unknown mutation " + kind)
|
||||
return wf
|
||||
|
||||
|
||||
wf = yaml.safe_load(open(sys.argv[2]))
|
||||
if sys.argv[1] == "mutate":
|
||||
yaml.safe_dump(mutate(wf, sys.argv[3]), sys.stdout)
|
||||
else:
|
||||
for b in audit(wf):
|
||||
print("BAD " + b)
|
||||
PY
|
||||
said=$("$py" "$tmp/wf.py" audit "$workflow") || fail "the workflow audit did not run: $said"
|
||||
grep -q '^BAD ' <<<"$said" && fail "$(grep '^BAD ' <<<"$said" | sed 's/^BAD //')"
|
||||
cap=$(($(sed -n 's/^timeout=//p' <<<"$said") * 60))
|
||||
|
||||
# One control per key, or an audit that stopped looking at one would ride on
|
||||
# another still firing. "none" round-trips the file through the parser, which
|
||||
# rewrites quoting, order and flow style: no verdict may move.
|
||||
for m in none:'' perm:'cannot post' jobperm:'cannot post' creds:'persists the job token' \
|
||||
env:'WATCHDOG_TOKEN is' token:'WATCHDOG_TOKEN is' sha:'WATCHDOG_SHA is' \
|
||||
context:'does not name matrix.platform' url:'does not link the run' \
|
||||
cap:'no timeout-minutes of its own'; do
|
||||
"$py" "$tmp/wf.py" mutate "$workflow" "${m%%:*}" >"$tmp/wf.yml"
|
||||
said=$("$py" "$tmp/wf.py" audit "$tmp/wf.yml") ||
|
||||
fail "the workflow audit did not run on the ${m%%:*} control: $said"
|
||||
if test -z "${m#*:}"; then
|
||||
grep -q '^BAD ' <<<"$said" && fail "a reformatted workflow fails the audit: $said"
|
||||
else
|
||||
grep -q "^BAD .*${m#*:}" <<<"$said" ||
|
||||
fail "the ${m%%:*} control did not trip the audit: $said"
|
||||
fi
|
||||
done
|
||||
|
||||
# The heartbeat still owns the kill and must beat the step timeout.
|
||||
stuck=$(sed -n 's/^stuck=\([0-9]*\)$/\1/p' "$driver")
|
||||
deadline=$(sed -n 's/^suite_deadline=\([0-9]*\)$/\1/p' "$driver")
|
||||
test -n "$stuck" || fail "cannot read the heartbeat's static window"
|
||||
test -n "$deadline" || fail "cannot read the driver's suite deadline"
|
||||
# A test may start one second under the deadline and never report again.
|
||||
test $((deadline + stuck + 240)) -le "$cap" ||
|
||||
fail "a kill at ${deadline}+${stuck}s leaves under 240s before the ${cap}s step timeout"
|
||||
|
||||
# The watchdog's own stop is its MaxSeconds, which must land inside the step.
|
||||
# shellcheck disable=SC2016 # $MaxSeconds is PowerShell's
|
||||
maxsecs=$(sed -n 's/^[[:space:]]*\[int\]\$MaxSeconds = \([0-9]*\).*$/\1/p' "$wdscript")
|
||||
test -n "$maxsecs" || fail "cannot read the watchdog's own deadline"
|
||||
test "$maxsecs" -le "$cap" || fail "the watchdog runs ${maxsecs}s past the ${cap}s step timeout"
|
||||
# Bounded below too: the reporter has to outlive the heartbeat's own kill.
|
||||
test "$maxsecs" -ge $((deadline + stuck)) ||
|
||||
fail "the watchdog stops at ${maxsecs}s, before the kill at $((deadline + stuck))s"
|
||||
fi
|
||||
|
||||
psruns=()
|
||||
for c in pwsh powershell.exe powershell; do
|
||||
command -v "$c" >/dev/null 2>&1 || continue
|
||||
# powershell and powershell.exe are one interpreter under two names.
|
||||
seen=0
|
||||
for p in ${psruns[@]+"${psruns[@]}"}; do
|
||||
test "${p%.exe}" != "${c%.exe}" || seen=1
|
||||
done
|
||||
test "$seen" -eq 1 || psruns+=("$c")
|
||||
done
|
||||
if test "${#psruns[@]}" -eq 0; then
|
||||
echo "SKIP: no PowerShell here, the watchdog's own self-test cannot run"
|
||||
exit 77
|
||||
fi
|
||||
|
||||
# Bounded: a mutant that loops forever would otherwise be caught only by the step
|
||||
# timeout this work exists to beat. -NoPost layers over the scrub above.
|
||||
run_wd() {
|
||||
local secs=$1
|
||||
shift
|
||||
# -NoPost last: everything after -File <script> belongs to the script.
|
||||
run_with_timeout "$secs" "$psrun" "${psargs[@]}" "$@" -NoPost 2>&1
|
||||
}
|
||||
|
||||
# The legs below grade what reached the API. Printing and posting are decoupled,
|
||||
# so counting what the loop wrote can see neither the request rate nor the backoff.
|
||||
sink="$tmp/sink.py"
|
||||
cat >"$sink" <<'PY'
|
||||
import http.server
|
||||
import socketserver
|
||||
import sys
|
||||
|
||||
codes = [int(c) for c in sys.argv[1].split(",")]
|
||||
rec = sys.argv[2]
|
||||
seen = []
|
||||
|
||||
|
||||
class Handler(http.server.BaseHTTPRequestHandler):
|
||||
def do_POST(self):
|
||||
n = int(self.headers.get("Content-Length") or 0)
|
||||
with open(rec, "ab") as f:
|
||||
f.write(self.rfile.read(n) + b"\n")
|
||||
seen.append(1)
|
||||
self.send_response(codes[(len(seen) - 1) % len(codes)])
|
||||
self.send_header("Content-Length", "2")
|
||||
self.end_headers()
|
||||
self.wfile.write(b"{}")
|
||||
|
||||
def log_message(self, *args):
|
||||
pass
|
||||
|
||||
|
||||
class Sink(http.server.HTTPServer):
|
||||
# Skip stock server_bind()'s getfqdn(): it stalls 35s on macOS (#870).
|
||||
def server_bind(self):
|
||||
socketserver.TCPServer.server_bind(self)
|
||||
self.server_name, self.server_port = self.server_address[:2]
|
||||
|
||||
|
||||
srv = Sink(("127.0.0.1", 0), Handler)
|
||||
# LF, since discover_server_port parses this line: Windows would emit \r\n.
|
||||
sys.stdout.reconfigure(newline="\n")
|
||||
print("PORT %d" % srv.server_port, flush=True)
|
||||
srv.serve_forever()
|
||||
PY
|
||||
|
||||
posts="$tmp/posts"
|
||||
sinklog="$tmp/sink.log"
|
||||
legout="$tmp/leg.out"
|
||||
port='' sinkpid=''
|
||||
sink_start() {
|
||||
: >"$posts"
|
||||
: >"$sinklog"
|
||||
# Its own log: a leg's command substitution would else wait on the pipe the
|
||||
# sink inherited for as long as the sink lives.
|
||||
"$py" "$sink" "$1" "$posts" >"$sinklog" 2>&1 &
|
||||
sinkpid=$!
|
||||
# Where the teardown can find it: sink_start runs in a subshell of its own.
|
||||
echo "$sinkpid" >"$sinkpidfile"
|
||||
port=$(discover_server_port "$sinklog" "$sinkpid") ||
|
||||
fail "the status sink never bound a port: $(cat "$sinklog")"
|
||||
}
|
||||
sink_stop() {
|
||||
stop_server "$sinkpid"
|
||||
: >"$sinkpidfile"
|
||||
sinkpid=''
|
||||
}
|
||||
# Ours, so a leg that lost its -ApiBase reaches GitHub with a credential it
|
||||
# refuses. Distinctive, since the legs grade the log on never carrying it.
|
||||
export WATCHDOG_TOKEN=n0tat0ken WATCHDOG_REPO=octo/nowhere WATCHDOG_SHA=0000000
|
||||
|
||||
# The log rides into the job's artifact, which nothing secret-masks.
|
||||
token_free() { ! grep -q "$WATCHDOG_TOKEN" "$1"; }
|
||||
|
||||
run_posting() {
|
||||
local secs=$1
|
||||
shift
|
||||
run_with_timeout "$secs" "$psrun" "${psargs[@]}" "$@" -ApiBase "http://127.0.0.1:$port"
|
||||
}
|
||||
# t and q of each posted status, in order and whatever key order the JSON took.
|
||||
posted_pairs() {
|
||||
sed -n 's/.*t=\([0-9]*\)s q=\([0-9?]*\)s.*/\1 \2/p' "$posts"
|
||||
}
|
||||
|
||||
# One property per leg, each returning its diagnosis so a mutant can be required
|
||||
# to trip that same check rather than merely to fail.
|
||||
cadence_leg() {
|
||||
local n
|
||||
sink_start 201
|
||||
printf 'RUN 36_local-bigcrawl.test at 41s\n' >"$tmp/progress.log"
|
||||
run_posting 60 -File "$1" -ProgressLog "$(nativepath "$tmp/progress.log")" \
|
||||
-IntervalSeconds 100 -PollSeconds 1 -MaxSeconds 4 >"$legout" 2>&1 || {
|
||||
sink_stop
|
||||
echo "the loop exited nonzero: $(cat "$legout")"
|
||||
return 1
|
||||
}
|
||||
sink_stop
|
||||
n=$(grep -c . "$posts" || true)
|
||||
test "$n" -eq 1 || {
|
||||
echo "$n API calls at a 100s cadence over 4s, want 1"
|
||||
return 1
|
||||
}
|
||||
grep -q '36_local-bigcrawl.test' "$posts" || {
|
||||
echo "the posted status does not name the test in flight: $(cat "$posts")"
|
||||
return 1
|
||||
}
|
||||
case $(posted_pairs) in
|
||||
*[0-9]' '[0-9]*) ;;
|
||||
*)
|
||||
echo "a readable log posted no staticness: $(posted_pairs)"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Staticness has to follow the log through movement and then through silence,
|
||||
# which is why this leg is the long one.
|
||||
staticness_leg() {
|
||||
local moved dq dt legpid stop
|
||||
sink_start 201
|
||||
printf 'RUN 36_local-bigcrawl.test at 0s\n' >"$tmp/progress.log"
|
||||
run_posting 90 -File "$1" -ProgressLog "$(nativepath "$tmp/progress.log")" \
|
||||
-IntervalSeconds 1 -PollSeconds 1 -MaxSeconds 16 >"$legout" 2>&1 &
|
||||
legpid=$!
|
||||
# Started on the first status that lands rather than on this shell's clock:
|
||||
# an interpreter three seconds into its own startup would see silence only,
|
||||
# and the moving half would go untested.
|
||||
for _ in $(seq 1 300); do
|
||||
test -s "$posts" && break
|
||||
sleep 0.1
|
||||
done
|
||||
stop=$((SECONDS + 3))
|
||||
while test "$SECONDS" -lt "$stop"; do
|
||||
printf 'RUN 36_local-bigcrawl.test at %ss\n' "$SECONDS" >>"$tmp/progress.log"
|
||||
sleep 0.4
|
||||
done
|
||||
wait "$legpid" || {
|
||||
sink_stop
|
||||
echo "the loop exited nonzero: $(cat "$legout")"
|
||||
return 1
|
||||
}
|
||||
sink_stop
|
||||
moved=$(posted_pairs | awk '$2 ~ /^[0-9]+$/ && $1 >= 2 && $2 <= 1 { n++ } END { print n + 0 }')
|
||||
test "$moved" -ge 1 || {
|
||||
echo "no post read the log as moving while it was being written: $(posted_pairs | tr '\n' '/')"
|
||||
return 1
|
||||
}
|
||||
# Once the log stops, staticness is elapsed time minus a constant, so it has
|
||||
# to track it one for one. A bound on it would report every wedge as the cap.
|
||||
dq=$(posted_pairs | awk '$1 >= 6 && $2 ~ /^[0-9]+$/ { if (f == "") f = $2; l = $2 } END { print l - f }')
|
||||
dt=$(posted_pairs | awk '$1 >= 6 && $2 ~ /^[0-9]+$/ { if (f == "") f = $1; l = $1 } END { print l - f }')
|
||||
if test "${dq:-0}" -ne "${dt:-0}" || test "${dq:-0}" -lt 4; then
|
||||
echo "staticness moved ${dq}s over ${dt}s of silence: $(posted_pairs | tr '\n' '/')"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Every due tick logs its line before the throttle decides, so these two counts
|
||||
# are a ratio no slow box can move: one to one while nothing is being skipped.
|
||||
throttle_leg() {
|
||||
local calls lines
|
||||
sink_start 201
|
||||
printf 'RUN 36_local-bigcrawl.test at 41s\n' >"$tmp/progress.log"
|
||||
run_posting 60 -File "$1" -ProgressLog "$(nativepath "$tmp/progress.log")" \
|
||||
-IntervalSeconds 1 -PollSeconds 1 -MaxSeconds 8 >"$legout" 2>&1 || {
|
||||
sink_stop
|
||||
echo "the loop exited nonzero: $(cat "$legout")"
|
||||
return 1
|
||||
}
|
||||
sink_stop
|
||||
calls=$(grep -c . "$posts" || true)
|
||||
lines=$(grep -c 't=[0-9]*s q=' "$legout" || true)
|
||||
test "$lines" -ge 3 || {
|
||||
echo "$lines due ticks over 8s at a 1s interval: too few to judge a throttle"
|
||||
return 1
|
||||
}
|
||||
test $((calls * 4)) -ge $((lines * 3)) || {
|
||||
echo "$calls of $lines due ticks reached an accepting API: a landed post throttles the next"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
backoff_leg() {
|
||||
local calls lines
|
||||
sink_start 403
|
||||
printf 'RUN 36_local-bigcrawl.test at 41s\n' >"$tmp/progress.log"
|
||||
run_posting 60 -File "$1" -ProgressLog "$(nativepath "$tmp/progress.log")" \
|
||||
-IntervalSeconds 1 -PollSeconds 1 -MaxSeconds 8 >"$legout" 2>&1 || {
|
||||
sink_stop
|
||||
echo "the loop exited nonzero: $(cat "$legout")"
|
||||
return 1
|
||||
}
|
||||
sink_stop
|
||||
calls=$(grep -c . "$posts" || true)
|
||||
lines=$(grep -c 't=[0-9]*s q=' "$legout" || true)
|
||||
test "$calls" -ge 2 || {
|
||||
echo "a rejecting API was called $calls times: the loop stopped trying"
|
||||
return 1
|
||||
}
|
||||
# One diagnosis for both: which one trips first depends on box speed. The
|
||||
# backoff must thin the calls, not the log; a token-less run leaves only the log.
|
||||
if test "$calls" -gt 5 || test "$lines" -lt $((2 * calls)); then
|
||||
echo "$calls calls and $lines log lines against an API rejecting every one: nothing backs off"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
fullstdout_leg() {
|
||||
local n
|
||||
sink_start 201
|
||||
printf 'RUN 36_local-bigcrawl.test at 41s\n' >"$tmp/progress.log"
|
||||
run_posting 60 -File "$1" -ProgressLog "$(nativepath "$tmp/progress.log")" \
|
||||
-IntervalSeconds 1 -PollSeconds 1 -MaxSeconds 5 >/dev/full 2>&1 || true
|
||||
sink_stop
|
||||
n=$(grep -c . "$posts" || true)
|
||||
test "$n" -ge 2 || {
|
||||
echo "$n posts with a full stdout: a failed log write took the loop with it"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
first=1
|
||||
for psrun in "${psruns[@]}"; do
|
||||
psargs=(-NoProfile -NonInteractive)
|
||||
case $psrun in
|
||||
powershell*) psargs+=(-ExecutionPolicy Bypass) ;;
|
||||
esac
|
||||
|
||||
out=$(run_wd 120 -File "$(nativepath "$wdscript")" -SelfTest) ||
|
||||
fail "the $psrun self-test failed: $out"
|
||||
grep -q 'watchdog self-test OK' <<<"$out" || fail "the $psrun self-test said: $out"
|
||||
|
||||
# A mutant the script survives is a property nothing is checking. Graded on
|
||||
# the assertion named: one that merely fails to parse would read as caught.
|
||||
mutate() {
|
||||
local name=$1 expr=$2 want=$3 m="$tmp/$1.ps1" rc=0 said
|
||||
sed "$expr" "$wdscript" >"$m"
|
||||
cmp -s "$wdscript" "$m" && fail "mutant $name changed nothing, so it proves nothing"
|
||||
said=$(run_wd 120 -File "$(nativepath "$m")" -SelfTest) || rc=$?
|
||||
test "$rc" -ne 0 || fail "mutant $name passed the $psrun self-test: $said"
|
||||
grep -q "self-test FAIL: .*$want" <<<"$said" ||
|
||||
fail "mutant $name tripped no assertion of its own: $said"
|
||||
}
|
||||
# shellcheck disable=SC2016 # PowerShell variables, quoted for sed
|
||||
mutate cadence-off-by-one 's/-ge \$Interval/-gt \$Interval/' 'due exactly on the interval'
|
||||
# shellcheck disable=SC2016
|
||||
mutate backoff-uncapped 's/\[Math\]::Min(\$Current \* 2, 32)/$Current * 2/' 'backoff is not capped'
|
||||
# shellcheck disable=SC2016
|
||||
mutate throttle-no-reset 's/if (\$Ok) { return @(0, 0) }/if ($Ok) { return @(0, $Backoff) }/' \
|
||||
'landed post leaves'
|
||||
# shellcheck disable=SC2016
|
||||
mutate status-unclipped '/if (\$s.Length -gt 140)/d' 'was not clipped'
|
||||
# An oversized status is cut from the tail: the head is what a wedge is read for.
|
||||
# shellcheck disable=SC2016
|
||||
mutate status-clipped-head 's/\$s = \$s.Substring(0, 140)/$s = $s.Substring($s.Length - 140)/' \
|
||||
'dropped the leading fields'
|
||||
# shellcheck disable=SC2016
|
||||
mutate tail-always-ok 's/\$r = @{ Ok = \$false;/$r = @{ Ok = $true;/' 'reads as one that was read'
|
||||
mutate counters-empty "s/return (\$f -join ' ')/return ''/" 'counters are not key=value'
|
||||
# The production cadence: no leg below runs without a schedule of its own.
|
||||
# shellcheck disable=SC2016
|
||||
mutate default-cadence 's/\[int\]\$IntervalSeconds = 30/[int]$IntervalSeconds = 3000/' \
|
||||
'default status cadence'
|
||||
# shellcheck disable=SC2016
|
||||
mutate default-poll 's/\[int\]\$PollSeconds = 5/[int]$PollSeconds = 50/' 'default poll'
|
||||
|
||||
# Timed and counted by this shell, never read out of what the watchdog
|
||||
# writes: the cadence and the deadline are both mine to set.
|
||||
printf 'RUN 36_local-bigcrawl.test at 41s\n' >"$tmp/progress.log"
|
||||
out=$(run_wd 60 -File "$(nativepath "$wdscript")" \
|
||||
-ProgressLog "$(nativepath "$tmp/progress.log")" \
|
||||
-IntervalSeconds 100 -PollSeconds 1 -MaxSeconds 5) ||
|
||||
fail "the $psrun slow-cadence loop exited nonzero: $out"
|
||||
slow=$(grep -c 't=[0-9]*s q=' <<<"$out" || true)
|
||||
test "$slow" -eq 1 || fail "$slow status lines at a 100s cadence over 5s, want 1: $out"
|
||||
|
||||
began=$SECONDS
|
||||
out=$(run_wd 60 -File "$(nativepath "$wdscript")" \
|
||||
-ProgressLog "$(nativepath "$tmp/progress.log")" \
|
||||
-IntervalSeconds 1 -PollSeconds 1 -MaxSeconds 5) ||
|
||||
fail "the $psrun loop exited nonzero: $out"
|
||||
spent=$((SECONDS - began))
|
||||
test "$spent" -ge 4 || fail "a 5s watchdog returned after ${spent}s: MaxSeconds is not seconds"
|
||||
test "$spent" -le 20 || fail "a 5s watchdog ran ${spent}s: it does not stop on its own"
|
||||
fast=$(grep -c 't=[0-9]*s q=' <<<"$out" || true)
|
||||
# Against the slow leg, not a floor: a probe tick is not instant.
|
||||
test "$fast" -gt "$slow" || fail "$fast status lines at 1s against $slow at 100s: $out"
|
||||
grep -q '36_local-bigcrawl.test' <<<"$out" || fail "the status does not name the test in flight: $out"
|
||||
grep -q '95_local-sitemap' <<<"$out" && fail "the status named a test that was never in flight"
|
||||
# Trips if the scrub and -NoPost were both lost: only a real attempt logs this.
|
||||
grep -q 'status post failed' <<<"$out" && fail "a test leg reached the status API"
|
||||
|
||||
# An unreadable log must not read as a wedged one.
|
||||
out=$(run_wd 60 -File "$(nativepath "$wdscript")" \
|
||||
-ProgressLog "$(nativepath "$tmp/no-such.log")" \
|
||||
-IntervalSeconds 1 -PollSeconds 1 -MaxSeconds 3) ||
|
||||
fail "the $psrun loop exited nonzero on a missing log: $out"
|
||||
grep -q 'q=?s' <<<"$out" || fail "a missing log reported a staticness: $out"
|
||||
|
||||
# The legs below read the sink, not the loop, and they exercise the script's
|
||||
# decisions rather than the interpreter's: one flavour is enough.
|
||||
test "$first" -eq 1 || continue
|
||||
first=0
|
||||
if test -z "$py"; then
|
||||
echo "note: no python3 here, the posting legs did not run"
|
||||
continue
|
||||
fi
|
||||
|
||||
# ENOSPC on stdout, a state the sick runner reaches. Measured, since the leg
|
||||
# rests on it: MSYS hands a native PowerShell a /dev/full it can write to.
|
||||
devfull=0
|
||||
# shellcheck disable=SC2016 # PowerShell source, not this shell's expansions
|
||||
if test -w /dev/full &&
|
||||
! run_with_timeout 60 "$psrun" "${psargs[@]}" \
|
||||
-Command '$ErrorActionPreference = "Stop"; Write-Host ("x" * 200000)' \
|
||||
>/dev/full 2>/dev/null; then
|
||||
devfull=1
|
||||
fi
|
||||
legs=(cadence throttle staticness backoff)
|
||||
test "$devfull" -eq 0 || legs+=(fullstdout)
|
||||
for leg in "${legs[@]}"; do
|
||||
: >"$legout"
|
||||
said=$("${leg}_leg" "$(nativepath "$wdscript")") ||
|
||||
fail "the $psrun watchdog fails the $leg leg: $said"
|
||||
token_free "$legout" || fail "the $leg leg logged the status token"
|
||||
done
|
||||
# Control: a catch block widened to the whole exception is how it would get out.
|
||||
leaky="$tmp/token-in-log.ps1"
|
||||
# shellcheck disable=SC2016 # PowerShell variables, quoted for sed
|
||||
sed '/status post failed/s/$_.Exception.Message/$script:Token/' "$wdscript" >"$leaky"
|
||||
cmp -s "$wdscript" "$leaky" && fail "the leaky mutant changed nothing, so it proves nothing"
|
||||
backoff_leg "$(nativepath "$leaky")" >/dev/null || true
|
||||
token_free "$legout" && fail "the token audit cannot see a logged token"
|
||||
|
||||
# Same shape as mutate() above, graded by a leg instead of the self-test, and
|
||||
# on the diagnosis rather than the exit status for the same reason.
|
||||
mutate_leg() {
|
||||
local name=$1 expr=$2 leg=$3 want=$4 m="$tmp/$1.ps1" said
|
||||
sed "$expr" "$wdscript" >"$m"
|
||||
cmp -s "$wdscript" "$m" && fail "mutant $name changed nothing, so it proves nothing"
|
||||
said=$("${leg}_leg" "$(nativepath "$m")") &&
|
||||
fail "mutant $name passed the $leg leg"
|
||||
grep -q "$want" <<<"$said" || fail "mutant $name tripped no check of its own: $said"
|
||||
}
|
||||
mutate_leg status-every-poll \
|
||||
"s/if ((Get-WatchdogAction \$now \$postedAt \$IntervalSeconds) -eq 'post')/if (\$true)/" \
|
||||
cadence 'API calls at a 100s cadence'
|
||||
# shellcheck disable=SC2016 # PowerShell variables, quoted for sed
|
||||
mutate_leg static-unknown 's/\$static = \$now - \$movedAt/$static = -1/' \
|
||||
cadence 'posted no staticness'
|
||||
# shellcheck disable=SC2016
|
||||
mutate_leg static-elapsed 's/\$static = \$now - \$movedAt/$static = $now/' \
|
||||
staticness 'read the log as moving'
|
||||
# shellcheck disable=SC2016
|
||||
mutate_leg static-capped 's/if (\$tail.Ok) { \$static = \$now - \$movedAt }/if ($tail.Ok) { $static = [Math]::Min($now - $movedAt, 5) }/' \
|
||||
staticness 'staticness moved'
|
||||
# shellcheck disable=SC2016
|
||||
mutate_leg skip-when-none-owed 's/if (\$skip -gt 0)/if ($skip -ge 0)/' \
|
||||
throttle 'a landed post throttles the next'
|
||||
# shellcheck disable=SC2016
|
||||
mutate_leg success-skips 's/if (\$Ok) { return @(0, 0) }/if ($Ok) { return @(1, 0) }/' \
|
||||
throttle 'a landed post throttles the next'
|
||||
# shellcheck disable=SC2016
|
||||
mutate_leg backoff-never-skips 's/if (\$skip -gt 0)/if ($false)/' backoff 'nothing backs off'
|
||||
if test "$devfull" -eq 1; then
|
||||
mutate_leg log-write-fatal 's/try { \(Write-Host .*\) } catch { }/\1/' \
|
||||
fullstdout 'took the loop with it'
|
||||
fi
|
||||
done
|
||||
|
||||
echo "native watchdog OK (${psruns[*]})"
|
||||
151
tests/227_watchdog-poll.test
Normal file
151
tests/227_watchdog-poll.test
Normal file
@@ -0,0 +1,151 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# testlib's polling helpers: the deadline tick every watchdog turns on, and the
|
||||
# wait for a fixture server's port line. Both used to fork a process per tick,
|
||||
# which is what the whole harness is built out of (#795).
|
||||
|
||||
set -eu
|
||||
|
||||
testdir=$(cd "$(dirname "$0")" && pwd)
|
||||
# shellcheck source=tests/testlib.sh
|
||||
. "${testdir}/testlib.sh"
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
tmp=$(mktemp -d "${TMPDIR:-/tmp}/httrack_poll.XXXXXX") || exit 1
|
||||
# A colon-free root for the PATH shim below: MSYS hands out a drive-letter TMPDIR
|
||||
# and a PATH entry carrying a colon is read as two, which left every count at zero.
|
||||
shim=$(mktemp -d /tmp/httrack_pollshim.XXXXXX 2>/dev/null) || shim="$tmp/count"
|
||||
trap 'set +e; rm -rf "$tmp" "$shim"' EXIT
|
||||
trap 'rm -rf "$tmp" "$shim"' HUP INT QUIT PIPE TERM
|
||||
|
||||
# --- the tick waits, and costs no process where the fd timer works -----------
|
||||
count_sleeps() { # count_sleeps <dir> <log>: a sleep shim recording each call
|
||||
mkdir -p "$1"
|
||||
printf '#!/bin/sh\necho tick >>"%s"\nexec %s "$@"\n' "$2" "$(command -v sleep)" >"$1/sleep"
|
||||
chmod +x "$1/sleep"
|
||||
PATH="$1:$PATH"
|
||||
export PATH
|
||||
# Proved, not assumed: an unreachable shim counts zero sleeps, which reads as
|
||||
# "nothing forked" and would pass the very assertions it silently disarmed.
|
||||
: >"$2"
|
||||
sleep 0
|
||||
test -s "$2" || fail "the sleep shim is not on PATH (entry '$1')"
|
||||
}
|
||||
ticks() { # ticks <label> <secs> <count> <floor>: polls, counting the sleeps forked
|
||||
local began=$SECONDS
|
||||
local i=0
|
||||
: >"$tmp/sleeps"
|
||||
while test "$i" -lt "$3"; do
|
||||
poll_wait "$2"
|
||||
i=$((i + 1))
|
||||
done
|
||||
# Floored at the whole wait asked for, so a tick running at half speed fails
|
||||
# too, not only one that returns at once and spins every deadline loop.
|
||||
test "$((SECONDS - began))" -ge "$4" || fail "$1: $3 polls of ${2}s took under ${4}s"
|
||||
}
|
||||
sleeps() { wc -l <"$tmp/sleeps" | tr -d ' '; }
|
||||
# Probed, not assumed: a host that cannot make a fifo -- and Windows, which opts
|
||||
# out -- is entitled to the forked tick, but one that can and took it anyway has
|
||||
# lost the whole point. The override counts as opting out too, or the escape hatch
|
||||
# for a host whose fd tick misbehaves would red the suite it exists to rescue.
|
||||
canfifo=
|
||||
if test -z "${HTTRACK_POLL_SLEEP:-}" && ! is_windows && mkfifo "$tmp/probe" 2>/dev/null; then
|
||||
canfifo=1
|
||||
fi
|
||||
(
|
||||
count_sleeps "$shim" "$tmp/sleeps"
|
||||
poll_wait 0.1 # settles POLL_STATE before the count
|
||||
echo "poll tick: $POLL_STATE"
|
||||
ticks "$POLL_STATE tick" 0.1 30 3
|
||||
test -z "$canfifo" || test "$POLL_STATE" = fifo ||
|
||||
fail "the tick fell back to sleep on a host with working fifos"
|
||||
if test "$POLL_STATE" = fifo; then
|
||||
if test "${BASH_VERSINFO[0]}" -ge 4; then
|
||||
test "$(sleeps)" -eq 0 || fail "the fd tick forked $(sleeps) sleeps"
|
||||
else
|
||||
# No fractional read -t below bash 4, so the documented fallback is the sleep asked for.
|
||||
test "$(sleeps)" -eq 30 || fail "bash ${BASH_VERSINFO[0]} forked $(sleeps) of 30 fractional ticks"
|
||||
fi
|
||||
# The whole-second tick every watchdog runs on must take the fd on every bash:
|
||||
# where a fraction cannot, it is the only win left. Sized for ~3s either way,
|
||||
# bash 4 shortening its own wait to 0.1s.
|
||||
if test "${BASH_VERSINFO[0]}" -ge 4; then n=30; else n=3; fi
|
||||
ticks "fd 1s tick" 1 "$n" 3
|
||||
test "$(sleeps)" -eq 0 || fail "the 1s fd tick forked $(sleeps) sleeps"
|
||||
fi
|
||||
# Forced back to sleep(1), which is the only tick a shimmed sleep can reach.
|
||||
# POLL_STATE is left as it settled: a run already on the fd must give it up
|
||||
# when a test starves the poll mid-run, so the override is read per tick.
|
||||
export HTTRACK_POLL_SLEEP=1
|
||||
ticks "forced sleep tick" 0.1 30 3
|
||||
test "$(sleeps)" -eq 30 || fail "HTTRACK_POLL_SLEEP did not fork a sleep per tick"
|
||||
# A timer fd that stops timing out must fall back, not spin. Whole seconds: a
|
||||
# fraction never reaches the fd below bash 4.
|
||||
unset HTTRACK_POLL_SLEEP # the fd is what is under test here, not the override
|
||||
POLL_STATE=fifo
|
||||
exec 9<&-
|
||||
: >"$tmp/sleeps"
|
||||
poll_wait 1 2>/dev/null
|
||||
test -s "$tmp/sleeps" || fail "a dead timer fd did not fall back to sleep"
|
||||
test "$POLL_STATE" = forked || fail "a dead timer fd was kept ($POLL_STATE)"
|
||||
)
|
||||
|
||||
# --- the port wait outlasts a slow announcement ------------------------------
|
||||
# A cold Python start lags seconds on a loaded runner, so falling through early
|
||||
# would fail every crawl test on exactly the hosts the wait exists for.
|
||||
log="$tmp/late.log"
|
||||
: >"$log"
|
||||
(
|
||||
sleep 2
|
||||
echo "warning: something merged from stderr"
|
||||
echo "PORT 40001"
|
||||
sleep 30
|
||||
) >>"$log" 2>&1 &
|
||||
writer=$!
|
||||
began=$SECONDS
|
||||
port=$(discover_server_port "$log" "$writer") || fail "the late port line was missed"
|
||||
test "$port" = 40001 || fail "read port '$port', want 40001"
|
||||
test "$((SECONDS - began))" -ge 2 || fail "the port arrived before it was written"
|
||||
kill "$writer" 2>/dev/null || true
|
||||
wait "$writer" 2>/dev/null || true
|
||||
|
||||
# --- a half-written line is not a port ---------------------------------------
|
||||
# The announcement is one write in practice, but reading it in two would hand the
|
||||
# crawl a truncated port number, which resolves to a different server or none.
|
||||
log="$tmp/split.log"
|
||||
printf 'PORT 40' >"$log"
|
||||
(
|
||||
sleep 2
|
||||
printf '002\n'
|
||||
sleep 30
|
||||
) >>"$log" 2>&1 &
|
||||
writer=$!
|
||||
port=$(discover_server_port "$log" "$writer") || fail "the completed port line was missed"
|
||||
test "$port" = 40002 || fail "read port '$port' from a split line, want 40002"
|
||||
kill "$writer" 2>/dev/null || true
|
||||
wait "$writer" 2>/dev/null || true
|
||||
|
||||
# --- a server that dies without announcing is reported, not waited out -------
|
||||
log="$tmp/dead.log"
|
||||
echo "Traceback: address already in use" >"$log"
|
||||
(exit 1) &
|
||||
writer=$!
|
||||
wait "$writer" 2>/dev/null || true
|
||||
began=$SECONDS
|
||||
rc=0
|
||||
err=$(discover_server_port "$log" "$writer" 2>&1 >/dev/null) || rc=$?
|
||||
test "$rc" -ne 0 || fail "a dead server yielded a port"
|
||||
test "$((SECONDS - began))" -lt 20 || fail "a dead server was waited out ($((SECONDS - began))s)"
|
||||
case "$err" in
|
||||
*"exited early"*) ;;
|
||||
*) fail "a dead server reported '$err'" ;;
|
||||
esac
|
||||
# 2, not 1: only the deadline is the announce race 72 and 105 skip on, so one
|
||||
# status for both had local-crawl.sh skipping a crashed server instead of failing.
|
||||
test "$rc" -eq 2 || fail "a dead server exited $rc, want 2"
|
||||
|
||||
echo "poll helpers OK"
|
||||
46
tests/228_icon-small-flat.test
Normal file
46
tests/228_icon-small-flat.test
Normal file
@@ -0,0 +1,46 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Issue #938: downscaling the vector master to 16 px antialiased every stem to
|
||||
# grey. The small raster is drawn on the pixel grid, so it must stay flat.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
testdir=$(cd "$(dirname "$0")" && pwd)
|
||||
# shellcheck source=tests/testlib.sh
|
||||
. "${testdir}/testlib.sh"
|
||||
|
||||
srcdir="${abs_top_srcdir:?not run under make check}"
|
||||
div="${srcdir}/html/server/div"
|
||||
|
||||
python=$(find_python) || ! echo "python3 not found; skipping" >&2 || exit 77
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
colors() {
|
||||
"${python}" "${testdir}/png-colors.py" "${div}/$1/httrack.png" ||
|
||||
fail "$1: cannot read the icon"
|
||||
}
|
||||
|
||||
small=$(colors 16x16)
|
||||
n=$(wc -l <<<"${small}")
|
||||
test "${n}" -le 4 || fail "16x16 icon has ${n} colours, the stems are antialiased"
|
||||
|
||||
# Both flats present: an all-field tile and an all-ink one are flat too.
|
||||
for flat in '#9999cc' '#040404'; do
|
||||
grep -q " ${flat}\$" <<<"${small}" || fail "16x16 icon has no ${flat} pixel"
|
||||
done
|
||||
|
||||
# A hairline and a near-solid block both satisfy every count above.
|
||||
ink=$(sed -n "s/ #040404\$//p" <<<"${small}")
|
||||
if test "${ink}" -lt 26 || test "${ink}" -gt 128; then
|
||||
fail "16x16 icon inks ${ink} of 256 pixels"
|
||||
fi
|
||||
|
||||
# Control: a counter blind to antialiasing would pass every assertion above.
|
||||
big=$(wc -l <<<"$(colors 48x48)")
|
||||
test "${big}" -gt 16 || fail "48x48 icon has ${big} colours, the counter is blind"
|
||||
|
||||
echo "16x16 icon: ${n} colours, ${ink}/256 ink; 48x48: ${big} colours"
|
||||
94
tests/230_local-ftp-userpass.test
Executable file
94
tests/230_local-ftp-userpass.test
Executable file
@@ -0,0 +1,94 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# The credentials an URL carries must reach the control channel whole: a clip
|
||||
# logged in as a different account (#1032). The engine test only ever probes a
|
||||
# dead port, so nothing there reads what the login actually sends.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
: "${top_srcdir:=..}"
|
||||
testdir=$(cd "$(dirname "$0")" && pwd)
|
||||
# shellcheck source=tests/testlib.sh
|
||||
. "${testdir}/testlib.sh"
|
||||
|
||||
# Starves the x64 CI runner until the whole suite step is lost; Win32 is fine (#1038).
|
||||
if is_windows; then
|
||||
echo "windows: wedges the x64 runner (#1038), skipping"
|
||||
exit 77
|
||||
fi
|
||||
|
||||
python=$(find_python) || ! echo "python3 not found; skipping" >&2 || exit 77
|
||||
command -v httrack >/dev/null || {
|
||||
echo "could not find httrack" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
server=$(nativepath "${testdir}/ftp-server.py")
|
||||
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/httrack_ftpuser.XXXXXX")
|
||||
serverpid=
|
||||
cleanup() {
|
||||
stop_server "$serverpid"
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
trap 'set +e; cleanup' EXIT
|
||||
trap cleanup HUP INT QUIT PIPE TERM
|
||||
|
||||
root="${tmpdir}/root"
|
||||
out="${tmpdir}/crawl"
|
||||
cmds="${tmpdir}/cmds"
|
||||
mkdir -p "$root" "$out"
|
||||
printf 'body\n' >"${root}/f.txt"
|
||||
|
||||
serverlog="${tmpdir}/server.out"
|
||||
"$python" "$server" --root "$(nativepath "$root")" --require-pass \
|
||||
--log "$(nativepath "$cmds")" >"$serverlog" 2>&1 &
|
||||
serverpid=$!
|
||||
port=$(discover_server_port "$serverlog" "$serverpid") || exit 1
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
ok() { echo "OK: $*"; }
|
||||
rep() { awk -v n="$2" -v c="$1" 'BEGIN { while (i++ < n) printf "%s", c }'; }
|
||||
|
||||
crawl() {
|
||||
: >"$cmds"
|
||||
rm -rf "${out:?}"
|
||||
mkdir -p "$out"
|
||||
run_with_timeout 60 httrack "ftp://$1@127.0.0.1:${port}/f.txt" -O "$out" \
|
||||
--quiet --disable-security-limits --robots=0 --timeout=20 \
|
||||
--max-time=45 --retries=1 -c1 >"${tmpdir}/log" 2>&1
|
||||
}
|
||||
|
||||
# --- a plain login goes out unchanged ----------------------------------------
|
||||
crawl "bob:secret" || fail "the plain-login crawl never finished"
|
||||
sent=$(cat "$cmds")
|
||||
grep -qxF "USER bob" <<<"$sent" || fail "no USER bob on the wire: ${sent}"
|
||||
grep -qxF "PASS secret" <<<"$sent" || fail "no PASS secret on the wire: ${sent}"
|
||||
cmp -s "${out}/127.0.0.1_${port}/f.txt" "${root}/f.txt" ||
|
||||
fail "the login did not bring the body back; mirror holds $(find "$out" -type f)"
|
||||
ok "bob:secret reaches the server verbatim, and mirrors"
|
||||
|
||||
# --- the longest userinfo that fits, byte for byte ---------------------------
|
||||
# 255 of each: a 256th byte is refused, so this is what the wire must carry.
|
||||
user=$(rep a 255)
|
||||
pass=$(rep b 255)
|
||||
crawl "${user}:${pass}" || fail "the 255-byte login crawl never finished"
|
||||
sent=$(cat "$cmds")
|
||||
grep -qxF "USER ${user}" <<<"$sent" ||
|
||||
fail "the wire user is not the 255 bytes the URL named: $(grep -a '^USER' <<<"$sent")"
|
||||
grep -qxF "PASS ${pass}" <<<"$sent" ||
|
||||
fail "the wire pass is not the 255 bytes the URL named: $(grep -a '^PASS' <<<"$sent")"
|
||||
ok "a 255-byte user and password reach the server unclipped"
|
||||
|
||||
# --- over-long userinfo never opens a session --------------------------------
|
||||
# The bare name has no ':' to bound it, only the '@', and is the worse branch.
|
||||
for u in "$(rep a 256):pw" "$(rep a 256)"; do
|
||||
crawl "$u" || fail "the over-long crawl never finished"
|
||||
grep -aq "FTP user name or password too long" "${out}/hts-log.txt" ||
|
||||
fail "over-long userinfo was not refused: $(cat "${out}/hts-log.txt")"
|
||||
sent=$(cat "$cmds")
|
||||
test -z "$sent" || fail "the engine logged in under a clipped name: ${sent}"
|
||||
done
|
||||
ok "an over-long name is refused before anything reaches the control channel"
|
||||
82
tests/231_test-names.test
Executable file
82
tests/231_test-names.test
Executable file
@@ -0,0 +1,82 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# The tree's own names, the build dir's TESTS against them, and one control per
|
||||
# way either check can go blind.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
testdir=$(cd "$(dirname "$0")" && pwd)
|
||||
checker="${testdir}/check-test-names.sh"
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
work=$(mktemp -d "${TMPDIR:-/tmp}/test_names.XXXXXX") || fail "no tmpdir"
|
||||
trap 'set +e; rm -rf "$work"' EXIT
|
||||
|
||||
bash "$checker" "$testdir" || fail "the tree's own test names do not check out"
|
||||
|
||||
## the build dir runs the TESTS configure froze into it, not what tests/ holds
|
||||
#
|
||||
# Nothing regenerates that list when a .test appears, so without this the new
|
||||
# test is simply never run and every other test still passes.
|
||||
mk="${abs_top_builddir:-}/tests/Makefile"
|
||||
if [ -z "${abs_top_builddir:-}" ]; then
|
||||
echo "note: abs_top_builddir unset, skipping the stale-TESTS check (run via make check)" >&2
|
||||
elif [ ! -r "$mk" ]; then
|
||||
fail "no Makefile at $mk"
|
||||
else
|
||||
want=$(cd "$testdir" && printf '%s\n' [0-9]*_*.test | sort)
|
||||
got=$(sed -n 's/^TESTS = //p' "$mk" | tr ' ' '\n' | sed '/^$/d' | sort)
|
||||
test -n "$got" || fail "no TESTS assignment in $mk"
|
||||
if [ "$want" != "$got" ]; then
|
||||
fail "this build dir's TESTS predates tests/; re-run configure.
|
||||
never run: $(comm -23 <(printf '%s\n' "$want") <(printf '%s\n' "$got") | tr '\n' ' ')
|
||||
gone from tests/: $(comm -13 <(printf '%s\n' "$want") <(printf '%s\n' "$got") | tr '\n' ' ')"
|
||||
fi
|
||||
fi
|
||||
|
||||
## the checker still sees each way it can break
|
||||
#
|
||||
# It is silent on a clean tree, so one that had stopped looking would pass the
|
||||
# assertion above just as well.
|
||||
control() {
|
||||
local name=$1 want=$2 mutate=$3 dir="${work}/$1" out=""
|
||||
mkdir -p "$dir"
|
||||
: >"${dir}/00_kept.test"
|
||||
(cd "$dir" && eval "$mutate")
|
||||
if out=$(bash "$checker" "$dir" 2>&1); then
|
||||
fail "$name: the checker accepted a name make check would not run"
|
||||
fi
|
||||
grep -q "$want" <<<"$out" || fail "$name: wrong diagnostic: $out"
|
||||
}
|
||||
|
||||
control misnamed 'outside the' ': >ghost.test'
|
||||
control noprefix 'outside the' ': >engine-thing.test'
|
||||
control uppercase 'outside the' ': >01_Engine.TEST'
|
||||
control whitespace 'whitespace in the name' ': >"01_a b.test"'
|
||||
control directory 'not a regular file' 'mkdir 99_dir.test'
|
||||
control dangling 'not a regular file' 'ln -s nowhere 99_dangling.test'
|
||||
control empty 'no test matched' 'rm 00_kept.test'
|
||||
|
||||
# A git that answers with an error must report that the oracle did not run, not
|
||||
# read as "nothing tracked". Stubbed rather than dropped from PATH, which the
|
||||
# checker's other tools need.
|
||||
mkdir -p "$work/stub"
|
||||
printf '#!/bin/sh\nexit 1\n' >"$work/stub/git"
|
||||
chmod +x "$work/stub/git"
|
||||
out=$(PATH="$work/stub:$PATH" bash "$checker" "$testdir" 2>&1) ||
|
||||
fail "the checker failed outright with git stubbed out: $out"
|
||||
grep -q 'skipping the tracked-set check' <<<"$out" ||
|
||||
fail "a git that cannot answer read as a clean tracked set: $out"
|
||||
|
||||
if command -v git >/dev/null 2>&1; then
|
||||
control untracked 'untracked' 'git init -q . && git add 00_kept.test && : >99_ghost.test'
|
||||
control gone 'tracked but absent' 'git init -q . && : >99_extra.test && git add . && rm 99_extra.test'
|
||||
else
|
||||
echo "note: no git, skipping the tracked-set controls" >&2
|
||||
fi
|
||||
|
||||
echo "OK: tests/, the build dir's TESTS and the checker's blind spots all line up"
|
||||
172
tests/232_online-gate-outoftree.test
Executable file
172
tests/232_online-gate-outoftree.test
Executable file
@@ -0,0 +1,172 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# The online crawl gates must resolve their helper scripts against the script's
|
||||
# own directory: an out-of-tree build tree holds no copy, so a bare relative
|
||||
# path fails to open and the tests skip claiming the network is unavailable
|
||||
# (#1016). Everything here runs from an unrelated directory with the flag off,
|
||||
# so it never touches the network.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# 226 asks this of every test naming the Windows suite driver, as the sweep's
|
||||
# allowlist below does.
|
||||
unset WATCHDOG_TOKEN WATCHDOG_REPO WATCHDOG_SHA WATCHDOG_URL
|
||||
|
||||
testdir=$(cd "$(dirname "$0")" && pwd)
|
||||
|
||||
work=$(mktemp -d)
|
||||
cleanup() {
|
||||
rm -rf "$work"
|
||||
}
|
||||
trap 'set +e; cleanup' EXIT
|
||||
trap 'set +e; cleanup; exit 1' HUP INT QUIT TERM
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
## the gated tests must skip for the reason the flag gave, not because the gate
|
||||
## script could not be opened
|
||||
|
||||
# Discovered rather than listed, so a new online test is covered too. Matches the
|
||||
# gate line itself, not the mentions of it in comments.
|
||||
gated=$(cd "$testdir" && command grep -lE '^[[:space:]]*[^#[:space:]].*check-network\.sh.*exit 77' -- *.test | sort)
|
||||
count=$(printf '%s\n' "$gated" | command grep -c . || true)
|
||||
# Floor: the seven of #1016. A silently empty list would pass every check below.
|
||||
test "$count" -ge 7 || fail "found only $count tests gating on check-network.sh, expected at least 7"
|
||||
|
||||
for t in $gated; do
|
||||
out=$(cd "$work" && ONLINE_UNIT_TESTS=no bash "$testdir/$t" 2>&1) && rc=0 || rc=$?
|
||||
test "$rc" -eq 77 || fail "$t exited $rc from an out-of-tree cwd, expected 77 (skip)"
|
||||
# Only check-network.sh emits this, and only once it has actually run. The
|
||||
# "skipping online unit tests" banner prints on the broken path too, so
|
||||
# matching that instead would assert nothing.
|
||||
command grep -q 'online tests are disabled' <<<"$out" ||
|
||||
fail "$t skipped without the gate's reason, got: $out"
|
||||
done
|
||||
|
||||
## the flag still decides, from an out-of-tree cwd
|
||||
|
||||
out=$(cd "$work" && ONLINE_UNIT_TESTS=no bash "$testdir/check-network.sh" 2>&1) && rc=0 || rc=$?
|
||||
test "$rc" -eq 1 || fail "check-network.sh exited $rc with the flag off, expected 1"
|
||||
command grep -q 'online tests are disabled' <<<"$out" || fail "check-network.sh gave no reason: $out"
|
||||
|
||||
out=$(cd "$work" && ONLINE_UNIT_TESTS=yes bash "$testdir/check-network.sh" 2>&1) && rc=0 || rc=$?
|
||||
test "$rc" -eq 0 || fail "check-network.sh exited $rc with the flag on, expected 0: $out"
|
||||
|
||||
## no helper anywhere in tests/ is invoked through the caller's cwd
|
||||
#
|
||||
# The gate above cannot see this class: with the flag off check-network.sh exits
|
||||
# before a test's own crawl-test.sh line is ever reached, so the sweep is the
|
||||
# only thing standing between #1016 and a rerun of it.
|
||||
|
||||
# An invocation is safe when its operand resolves the same from any cwd: an
|
||||
# absolute path, or one built from a variable holding a directory. Quotes and a
|
||||
# leading "./" change nothing about what the shell opens, so neither is exempt.
|
||||
invoke='(^|[[:space:];&|(`])(bash|sh|source|\.)[[:space:]]+'
|
||||
quote='["'"'"']?'
|
||||
literal_re="$invoke$quote"'(\./)?[A-Za-z0-9_][A-Za-z0-9_./-]*\.sh'
|
||||
var_re="$invoke$quote"'\$\{?[A-Za-z_][A-Za-z0-9_]*\}?'"$quote"'([[:space:]]|$)'
|
||||
# Anchored at a command position, or the word inside these failure messages
|
||||
# would match and this file would pin itself.
|
||||
eval_re='(^|[;&|(`])[[:space:]]*eval[[:space:]]'
|
||||
|
||||
# The only two call sites that mean the cwd: each cd's into a directory it has
|
||||
# just filled with copies, and the copy is the target. Named rather than matched
|
||||
# by shape, because a shape exemption cannot tell them from #1016 in disguise.
|
||||
allowed='172_ci-windows-driver.test ci-windows-suite.sh
|
||||
ci-windows-suite.sh test-timeout.sh'
|
||||
|
||||
# Matching lines of the given files as file:line:text, comments dropped:
|
||||
# local-crawl.sh documents its own usage with a bare call.
|
||||
code_lines() {
|
||||
local re=$1
|
||||
shift
|
||||
command grep -HnE "$re" -- "$@" | command grep -vE '^[^:]*:[0-9]+:[[:space:]]*#' || true
|
||||
}
|
||||
|
||||
# Prints every invocation resolved against the caller's cwd, and every one whose
|
||||
# operand it cannot resolve. Silence means the files are clean.
|
||||
sweep() {
|
||||
local hit file text script var val
|
||||
|
||||
while IFS= read -r hit; do
|
||||
test -n "$hit" || continue
|
||||
file=${hit%%:*}
|
||||
text=${hit#*:}
|
||||
text=${text#*:}
|
||||
script=$(command grep -oE '[A-Za-z0-9_][A-Za-z0-9_./-]*\.sh' <<<"$text" | head -1)
|
||||
command grep -qxF "${file##*/} $script" <<<"$allowed" || echo "cwd-relative: $hit"
|
||||
done < <(code_lines "$literal_re" "$@")
|
||||
|
||||
# A variable operand is only as good as what it holds, so resolve it: an
|
||||
# assignment that is not itself anchored, or that cannot be found in the
|
||||
# file at all, is reported rather than assumed innocent.
|
||||
while IFS= read -r hit; do
|
||||
test -n "$hit" || continue
|
||||
file=${hit%%:*}
|
||||
text=${hit#*:}
|
||||
text=${text#*:}
|
||||
var=$(command grep -oE "$var_re" <<<"$text" | head -1 |
|
||||
sed -E 's/.*\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?.*/\1/')
|
||||
val=$(command grep -hE "^[[:space:]]*$var=" "$file" | head -1)
|
||||
val=${val#*=}
|
||||
val=${val#[\"\']}
|
||||
case $val in
|
||||
/* | \$*) ;;
|
||||
*) echo "unresolved \$$var: $hit" ;;
|
||||
esac
|
||||
done < <(code_lines "$var_re" "$@")
|
||||
}
|
||||
|
||||
# eval hides its operand from any static reading, so the set of them is pinned
|
||||
# instead: a new one fails here and gets a human's eye rather than passing unseen.
|
||||
scan_eval() {
|
||||
code_lines "$eval_re" "$@" |
|
||||
sed -E 's#^\./##; s/^([^:]*):[0-9]+:[[:space:]]*(.*[[:space:]])?eval[[:space:]]+/\1 /'
|
||||
}
|
||||
|
||||
# Controls. Each evasion is a printf format here, so the forms under test reach
|
||||
# the fixtures without appearing in this file as invocations the sweep would see.
|
||||
name=crawl-test.sh
|
||||
driver=ci-windows-suite.sh
|
||||
ev=$work/evasions
|
||||
mkdir -p "$ev"
|
||||
printf '#!/bin/bash\nbash %s --errors 0\n' "$name" >"$ev/bare.sh"
|
||||
printf '#!/bin/bash\nbash ./%s --errors 0\n' "$name" >"$ev/dotslash.sh"
|
||||
printf '#!/bin/bash\nbash "%s" --errors 0\n' "$name" >"$ev/quoted.sh"
|
||||
# shellcheck disable=SC2016 # the fixture's text, not this shell's
|
||||
printf '#!/bin/bash\nS=%s\nbash $S --errors 0\n' "$name" >"$ev/indirect.sh"
|
||||
# shellcheck disable=SC2016 # the fixture's text, not this shell's
|
||||
printf '#!/bin/bash\nbash ./%s "$bin"\n' "$driver" >"$ev/impostor.sh"
|
||||
# shellcheck disable=SC2016 # the fixture's text, not this shell's
|
||||
printf '#!/bin/bash\nG="$testdir/%s"\nbash $G --errors 0\n' "$name" >"$ev/anchored.sh"
|
||||
printf '#!/bin/bash\n# bash %s --errors 0\n' "$name" >"$ev/commented.sh"
|
||||
|
||||
for f in bare dotslash quoted indirect impostor; do
|
||||
test -n "$(sweep "$ev/$f.sh")" || fail "the sweep let $f through: $(tail -n +2 "$ev/$f.sh")"
|
||||
done
|
||||
# The other half of the control: a rule that flags everything proves nothing.
|
||||
for f in anchored commented; do
|
||||
out=$(sweep "$ev/$f.sh")
|
||||
test -z "$out" || fail "the sweep flagged $f.sh, which is correct code: $out"
|
||||
done
|
||||
printf '#!/bin/bash\neval "bash %s"\n' "$name" >"$ev/eval.sh"
|
||||
test -n "$(scan_eval "$ev/eval.sh")" || fail "the eval scan missed a planted eval"
|
||||
|
||||
hits=$(cd "$testdir" && sweep ./*.test ./*.sh)
|
||||
test -z "$hits" || fail "helper scripts reachable only from the right cwd:
|
||||
$hits"
|
||||
|
||||
# Pinned by operand, since none of these builds a command naming a helper today.
|
||||
# shellcheck disable=SC2016 # the pinned operands, quoted as they appear
|
||||
reviewed='145_webhttrack-datadir.test "${line}"
|
||||
231_test-names.test "$mutate")
|
||||
61_webhttrack-locale.test "${block}"
|
||||
61_webhttrack-locale.test "${funcs}"'
|
||||
got=$(cd "$testdir" && scan_eval ./*.test ./*.sh | sort)
|
||||
test "$got" = "$(sort <<<"$reviewed")" || fail "the set of eval sites changed; each hides its operand from the sweep and needs reading:
|
||||
$got"
|
||||
|
||||
echo "OK: $count gated tests skip with the flag's reason, and tests/ is free of cwd-relative helper calls"
|
||||
127
tests/233_local-ftp-ctrl-timeout.test
Executable file
127
tests/233_local-ftp-ctrl-timeout.test
Executable file
@@ -0,0 +1,127 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Exercises --timeout on the FTP control and data channels, with two bounds no single
|
||||
# hardcoded constant satisfies, plus --timeout=0 meaning unlimited rather than instant.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
: "${top_srcdir:=..}"
|
||||
testdir=$(cd "$(dirname "$0")" && pwd)
|
||||
# shellcheck source=tests/testlib.sh
|
||||
. "${testdir}/testlib.sh"
|
||||
|
||||
python=$(find_python) || ! echo "python3 not found; skipping" >&2 || exit 77
|
||||
command -v httrack >/dev/null || {
|
||||
echo "could not find httrack" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
server=$(nativepath "${testdir}/ftp-server.py")
|
||||
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/httrack_ftptmo.XXXXXX")
|
||||
mutepid=
|
||||
livepid=
|
||||
cleanup() {
|
||||
stop_server "$mutepid"
|
||||
stop_server "$livepid"
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
trap 'set +e; cleanup' EXIT
|
||||
trap cleanup HUP INT QUIT PIPE TERM
|
||||
|
||||
root="${tmpdir}/root"
|
||||
cmds="${tmpdir}/cmds"
|
||||
mkdir -p "$root"
|
||||
printf 'body\n' >"${root}/f.txt"
|
||||
# One holds the data connection open and says nothing; the other feeds it in
|
||||
# chunks 0.2s apart, so a zero-length wait cannot ride the gaps out.
|
||||
awk 'BEGIN { s = sprintf("%0999d", 0); for (i = 0; i < 100; i++) print s }' \
|
||||
>"${root}/stall.bin"
|
||||
awk 'BEGIN { s = sprintf("%0999d", 0); for (i = 0; i < 60; i++) print s }' \
|
||||
>"${root}/trick.bin"
|
||||
printf '/stall.bin stall\n/trick.bin trickle\n' >"${tmpdir}/modes"
|
||||
|
||||
mutelog="${tmpdir}/mute.out"
|
||||
"$python" "$server" --root "$(nativepath "$root")" --mute >"$mutelog" 2>&1 &
|
||||
mutepid=$!
|
||||
muteport=$(discover_server_port "$mutelog" "$mutepid") || exit 1
|
||||
|
||||
livelog="${tmpdir}/live.out"
|
||||
"$python" "$server" --root "$(nativepath "$root")" \
|
||||
--mode-file "$(nativepath "${tmpdir}/modes")" \
|
||||
--log "$(nativepath "$cmds")" >"$livelog" 2>&1 &
|
||||
livepid=$!
|
||||
liveport=$(discover_server_port "$livelog" "$livepid") || exit 1
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
ok() { echo "OK: $*"; }
|
||||
|
||||
# No --max-time anywhere below, so --timeout is the only thing that can end a run.
|
||||
crawl() {
|
||||
local tag=$1 to=$2 cap=$3 url=$4
|
||||
local out="${tmpdir}/out${tag}" rc=0
|
||||
|
||||
mkdir -p "$out"
|
||||
run_with_timeout "$cap" httrack "$url" -O "$out" --quiet \
|
||||
--disable-security-limits --robots=0 --timeout="$to" -c1 \
|
||||
>"${tmpdir}/log${tag}" 2>&1 || rc=$?
|
||||
# Only the cap and a signal are the crawl's own failure. A nonzero exit against
|
||||
# a deliberately broken server is not, and the assertions below judge the rest.
|
||||
test "$rc" -ne 124 || fail "the ${tag} run was still going at ${cap}s"
|
||||
test "$rc" -lt 128 || fail "the ${tag} run died on signal $((rc - 128))"
|
||||
}
|
||||
|
||||
# Every case names the timeout it saw, since htsftp.c prints the value actually in
|
||||
# force: that kills a hardcoded constant more directly than a timing bound does.
|
||||
logged() {
|
||||
grep -aq "$2" "${tmpdir}/out${1}/hts-log.txt" ||
|
||||
fail "no '$2' in the log: $(grep -a Error "${tmpdir}/out${1}/hts-log.txt" || true)"
|
||||
}
|
||||
|
||||
# --- control channel: a server that connects and never greets -----------------
|
||||
# The engine retries once, so a run costs about twice the value asked for.
|
||||
start=$SECONDS
|
||||
crawl 4 4 60 "ftp://127.0.0.1:${muteport}/f.txt"
|
||||
short=$((SECONDS - start))
|
||||
logged 4 'read timeout (4)'
|
||||
test "$short" -lt 18 || fail "--timeout=4 took ${short}s: the greeting read ignores it"
|
||||
ok "a 4s timeout ends the greeting read in ${short}s"
|
||||
|
||||
# The floor here sits above the ceiling above, so no one constant satisfies both.
|
||||
start=$SECONDS
|
||||
crawl 20 20 120 "ftp://127.0.0.1:${muteport}/f.txt"
|
||||
long=$((SECONDS - start))
|
||||
logged 20 'read timeout (20)'
|
||||
test "$long" -ge 20 ||
|
||||
fail "--timeout=20 gave up after ${long}s, short of the value asked for"
|
||||
test "$long" -lt 70 || fail "--timeout=20 took ${long}s"
|
||||
ok "a 20s timeout waits ${long}s: the wait tracks the value, not a constant"
|
||||
|
||||
# --- data channel: logged in, mid-body, and the server goes quiet -------------
|
||||
# The control channel is answered here, so only the body-transfer wait can hang.
|
||||
: >"$cmds"
|
||||
start=$SECONDS
|
||||
crawl stall 5 60 "ftp://127.0.0.1:${liveport}/stall.bin"
|
||||
stalled=$((SECONDS - start))
|
||||
sent=$(cat "$cmds")
|
||||
grep -q '^RETR ' <<<"$sent" || fail "the body transfer never started: ${sent}"
|
||||
logged stall 'Time out (5)'
|
||||
test "$stalled" -lt 40 ||
|
||||
fail "a stalled body transfer took ${stalled}s under --timeout=5"
|
||||
ok "a 5s timeout ends the body-transfer wait in ${stalled}s"
|
||||
|
||||
# --- --timeout=0 is unlimited, not zero --------------------------------------
|
||||
: >"$cmds"
|
||||
start=$SECONDS
|
||||
crawl zero 0 90 "ftp://127.0.0.1:${liveport}/trick.bin"
|
||||
zeroed=$((SECONDS - start))
|
||||
got="${tmpdir}/outzero/127.0.0.1_${liveport}/trick.bin"
|
||||
test -f "$got" || fail "--timeout=0 fetched nothing; mirror holds $(find \
|
||||
"${tmpdir}/outzero" -type f -not -path '*hts-cache*')"
|
||||
want=$(wc -c <"${root}/trick.bin")
|
||||
have=$(wc -c <"$got")
|
||||
test "$have" -eq "$want" ||
|
||||
fail "--timeout=0 gave up mid-transfer: ${have} of ${want} bytes"
|
||||
ok "--timeout=0 rides out a ${zeroed}s trickle and keeps all ${want} bytes"
|
||||
74
tests/234_local-ftp-maxtime.test
Executable file
74
tests/234_local-ftp-maxtime.test
Executable file
@@ -0,0 +1,74 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Exercises --max-time against an in-flight FTP transfer; --timeout is set far above
|
||||
# the cap so only the mirror deadline can end the run.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
: "${top_srcdir:=..}"
|
||||
testdir=$(cd "$(dirname "$0")" && pwd)
|
||||
# shellcheck source=tests/testlib.sh
|
||||
. "${testdir}/testlib.sh"
|
||||
|
||||
python=$(find_python) || ! echo "python3 not found; skipping" >&2 || exit 77
|
||||
command -v httrack >/dev/null || {
|
||||
echo "could not find httrack" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
server=$(nativepath "${testdir}/ftp-server.py")
|
||||
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/httrack_ftpmax.XXXXXX")
|
||||
serverpid=
|
||||
cleanup() {
|
||||
stop_server "$serverpid"
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
trap 'set +e; cleanup' EXIT
|
||||
trap cleanup HUP INT QUIT PIPE TERM
|
||||
|
||||
root="${tmpdir}/root"
|
||||
out="${tmpdir}/crawl"
|
||||
cmds="${tmpdir}/cmds"
|
||||
mkdir -p "$root" "$out"
|
||||
# Big enough that the served prefix leaves the transfer open and mid-body.
|
||||
awk 'BEGIN { s = sprintf("%0999d", 0); for (i = 0; i < 100; i++) print s }' \
|
||||
>"${root}/f.bin"
|
||||
printf '* stall\n' >"${tmpdir}/modes"
|
||||
|
||||
serverlog="${tmpdir}/server.out"
|
||||
"$python" "$server" --root "$(nativepath "$root")" \
|
||||
--mode-file "$(nativepath "${tmpdir}/modes")" \
|
||||
--log "$(nativepath "$cmds")" >"$serverlog" 2>&1 &
|
||||
serverpid=$!
|
||||
port=$(discover_server_port "$serverlog" "$serverpid") || exit 1
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
ok() { echo "OK: $*"; }
|
||||
|
||||
start=$SECONDS
|
||||
run_with_timeout 90 httrack "ftp://127.0.0.1:${port}/f.bin" -O "$out" --quiet \
|
||||
--disable-security-limits --robots=0 --timeout=200 --max-time=20 -c1 \
|
||||
>"${tmpdir}/log" 2>&1 || fail "the run did not finish in $((SECONDS - start))s"
|
||||
elapsed=$((SECONDS - start))
|
||||
|
||||
log="${out}/hts-log.txt"
|
||||
# Confirms the transfer had started; otherwise the wait under test never runs.
|
||||
sent=$(cat "$cmds")
|
||||
grep -q '^RETR ' <<<"$sent" ||
|
||||
fail "the transfer never started, so nothing was in flight: ${sent}"
|
||||
ok "the crawl was inside the body transfer when the cap hit"
|
||||
|
||||
test "$elapsed" -ge 15 ||
|
||||
fail "the run ended after ${elapsed}s, before --max-time could be reached"
|
||||
test "$elapsed" -lt 60 || fail "--max-time=20 left the run going for ${elapsed}s"
|
||||
ok "--max-time ends the in-flight transfer in ${elapsed}s"
|
||||
|
||||
grep -aq 'More than 20 seconds passed' "$log" ||
|
||||
fail "the mirror deadline is not what ended the run: $(grep -a Error "$log" || true)"
|
||||
# Proves the run really did carry a 200s timeout it could have waited out.
|
||||
grep -aq 'Time out (200)' "$log" ||
|
||||
fail "--timeout=200 was not the timeout in force: $(grep -a Error "$log" || true)"
|
||||
ok "the deadline beat a --timeout ten times its size"
|
||||
252
tests/235_local-resume-recovery.test
Executable file
252
tests/235_local-resume-recovery.test
Executable file
@@ -0,0 +1,252 @@
|
||||
#!/bin/bash
|
||||
# A resume the server answers with an unusable Content-Range must drop the
|
||||
# partial and re-fetch the whole file, once, without spending the retry budget.
|
||||
set -e
|
||||
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
|
||||
|
||||
# Pass 1 has to be interrupted mid-download to leave the temp-ref the resume
|
||||
# needs, and MSYS cannot signal a native exe.
|
||||
if is_windows; then
|
||||
echo "Windows: the pass-1 interrupt needs a signal MSYS cannot deliver, skipping"
|
||||
exit 77
|
||||
fi
|
||||
|
||||
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/httrack_resume.XXXXXX") || exit 1
|
||||
serverpid=
|
||||
crawlpid=
|
||||
cleanup() {
|
||||
set +e
|
||||
test -n "$crawlpid" && kill -9 "$crawlpid" 2>/dev/null
|
||||
stop_server "$serverpid"
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
trap 'set +e; cleanup' EXIT
|
||||
trap 'set +e; cleanup; exit 1' HUP INT QUIT PIPE TERM
|
||||
|
||||
serverlog="${tmpdir}/server.log"
|
||||
: >"$serverlog"
|
||||
# stdin off the terminal: run_with_timeout toggles job control, and a background
|
||||
# job that touches the tty is stopped with SIGTTIN.
|
||||
"$python" "$server" --root "$root" >"$serverlog" 2>&1 </dev/null &
|
||||
serverpid=$!
|
||||
port=$(discover_server_port "$serverlog" "$serverpid") || exit 1
|
||||
base="http://127.0.0.1:${port}"
|
||||
|
||||
which httrack >/dev/null || {
|
||||
echo "could not find httrack"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Read from the server's own definitions: two routes happening to share a body
|
||||
# length must not let one leg assert the other's. Bodies come back as a length.
|
||||
server_const() { # server_const <name>
|
||||
"$python" -c 'import re, sys
|
||||
src = open(sys.argv[1]).read()
|
||||
v = eval(re.search(r"^\s*%s = (.+)$" % sys.argv[2], src, re.M).group(1))
|
||||
print(v if isinstance(v, int) else len(v))' "$server" "$1"
|
||||
}
|
||||
resume_full=$(server_const RESUME206_BODY)
|
||||
crange_full=$(server_const CRANGE206_BODY)
|
||||
have=$(server_const RESUME206_HAVE) # what pass 1 leaves on disk
|
||||
|
||||
# Crawl until the temp-ref appears, then interrupt: that is the state a resume
|
||||
# needs (a partial plus the ref naming it).
|
||||
interrupt_pass1() { # interrupt_pass1 <out> <route> <extra httrack args...>
|
||||
local out=$1 route=$2
|
||||
shift 2
|
||||
local refdir="${out}/hts-cache/ref"
|
||||
httrack -O "$out" --quiet --disable-security-limits --robots=0 \
|
||||
--timeout=30 -c1 "$@" "${base}/${route}/index.html" \
|
||||
>"${out}.log1" 2>&1 &
|
||||
crawlpid=$!
|
||||
local _
|
||||
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 || true
|
||||
crawlpid=
|
||||
test -n "$(find "$refdir" -name '*.ref' 2>/dev/null)" || {
|
||||
echo "FAIL: no temp-ref survived pass 1; cannot drive the resume"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
blob_size() { # blob_size <out>
|
||||
local blob
|
||||
blob=$(find "$1" -name blob.bin 2>/dev/null | head -1)
|
||||
test -n "$blob" || {
|
||||
echo 0
|
||||
return
|
||||
}
|
||||
wc -c <"$blob"
|
||||
}
|
||||
|
||||
# --- #1052: --retries=0 must still recover -----------------------------------
|
||||
# The restart is the same transfer asked for differently, so it must not be
|
||||
# charged to the retry budget. Before the fix this deleted the file.
|
||||
printf '[--retries=0 recovers the whole file] ..\t'
|
||||
out="${tmpdir}/retry0"
|
||||
mkdir "$out"
|
||||
interrupt_pass1 "$out" resume206 --retries=0
|
||||
test "$(blob_size "$out")" -eq "$have" || {
|
||||
echo "FAIL: pass 1 left $(blob_size "$out") bytes, expected ${have}"
|
||||
exit 1
|
||||
}
|
||||
rc=0
|
||||
run_with_timeout 120 httrack -O "$out" --quiet --disable-security-limits \
|
||||
--robots=0 --timeout=30 -c1 --retries=0 --continue \
|
||||
"${base}/resume206/index.html" >"${out}.log2" 2>&1 || rc=$?
|
||||
test "$rc" -ne 124 || {
|
||||
echo "FAIL: the crawl never terminated"
|
||||
exit 1
|
||||
}
|
||||
log="${out}/hts-log.txt"
|
||||
# Without these the recovery could not have been exercised at all.
|
||||
grep -q 'Unusable partial-content range' <"$log" || {
|
||||
echo "FAIL: the resume was never rejected; the test proved nothing"
|
||||
exit 1
|
||||
}
|
||||
grep -q 'Restarting whole file' <"$log" || {
|
||||
echo "FAIL: the restart did not run outside the retry budget"
|
||||
exit 1
|
||||
}
|
||||
got=$(blob_size "$out")
|
||||
test "$got" -eq "$resume_full" || {
|
||||
echo "FAIL: blob.bin is ${got} bytes, expected ${resume_full}"
|
||||
exit 1
|
||||
}
|
||||
echo "OK (${got} bytes)"
|
||||
|
||||
# --- the latch bounds an always-unusable server ------------------------------
|
||||
# This server answers every request unusably, with or without a Range, so an
|
||||
# ungated restart would requeue forever. Exactly one is free; the rest fall
|
||||
# back to the retry budget, which --retries=0 empties immediately.
|
||||
printf '[always-unusable server terminates] ..\t'
|
||||
out="${tmpdir}/loop"
|
||||
mkdir "$out"
|
||||
interrupt_pass1 "$out" resume206loop --retries=0
|
||||
rc=0
|
||||
run_with_timeout 120 httrack -O "$out" --quiet --disable-security-limits \
|
||||
--robots=0 --timeout=30 -c1 --retries=0 --continue \
|
||||
"${base}/resume206loop/index.html" >"${out}.log2" 2>&1 || rc=$?
|
||||
test "$rc" -ne 124 || {
|
||||
echo "FAIL: the crawl never terminated; the restart is unbounded"
|
||||
exit 1
|
||||
}
|
||||
grep -q 'Unusable partial-content range' <"${out}/hts-log.txt" || {
|
||||
echo "FAIL: the resume was never rejected; the latch was not exercised"
|
||||
exit 1
|
||||
}
|
||||
restarts=$(grep -c 'Restarting whole file' <"${out}/hts-log.txt" || true)
|
||||
test "$restarts" -eq 1 || {
|
||||
echo "FAIL: ${restarts} free restarts, expected exactly 1 (latch)"
|
||||
exit 1
|
||||
}
|
||||
echo "OK (1 free restart, then bounded)"
|
||||
|
||||
# --- the latch rides a re-record, and the budget survives the restart --------
|
||||
# A redirect to a case-different alias re-records the link under a new heap
|
||||
# entry; a latch left behind there buys a free restart every hop, so this
|
||||
# server (206, redirect, 206, ...) never ends. --retries=1 also shows the free
|
||||
# restart left the budget for the genuine failure that follows.
|
||||
printf '[alias redirect cannot reset the latch] ..\t'
|
||||
out="${tmpdir}/alias"
|
||||
mkdir "$out"
|
||||
rc=0
|
||||
run_with_timeout 120 httrack -O "$out" --quiet --disable-security-limits \
|
||||
--robots=0 --timeout=30 -c1 --retries=1 \
|
||||
"${base}/alias206/index.html" >"${out}.log1" 2>&1 || rc=$?
|
||||
test "$rc" -ne 124 || {
|
||||
echo "FAIL: the crawl never terminated; the alias hop reset the latch"
|
||||
exit 1
|
||||
}
|
||||
log="${out}/hts-log.txt"
|
||||
# Without the hop the alternation was never driven and the rest proves nothing.
|
||||
grep -q 'Warning moved treated' <"$log" || {
|
||||
echo "FAIL: no alias hop; the re-record path was never reached"
|
||||
exit 1
|
||||
}
|
||||
restarts=$(grep -c 'Restarting whole file' <"$log" || true)
|
||||
test "$restarts" -eq 1 || {
|
||||
echo "FAIL: ${restarts} free restarts across the alias hops, expected 1"
|
||||
exit 1
|
||||
}
|
||||
retries=$(grep -c 'Retry after error' <"$log" || true)
|
||||
test "$retries" -eq 1 || {
|
||||
echo "FAIL: ${retries} retries, expected 1; the free restart spent the budget"
|
||||
exit 1
|
||||
}
|
||||
echo "OK (1 free restart, budget intact)"
|
||||
|
||||
# --- #581: the refetch must not depend on the removal succeeding -------------
|
||||
# Windows refuses to unlink a file it still holds open, so the partial outlives
|
||||
# the restart. LD_PRELOAD is the only way to reach that from POSIX, and the
|
||||
# interposer needs /proc to tell an open handle from a closed one.
|
||||
if [ "$(uname -s)" != "Linux" ] || [ ! -d /proc/self/fd ]; then
|
||||
echo "resume-recovery: unlink interposition needs Linux + /proc, skipping the last leg"
|
||||
exit 0
|
||||
fi
|
||||
# A --disable-shared build has nothing to preload; anything else missing is a
|
||||
# build problem, not a skip, or this leg would pass vacuously.
|
||||
if [ ! -r "${UNLINKFAIL_LA:-}" ]; then
|
||||
echo "resume-recovery: ${UNLINKFAIL_LA:-\$UNLINKFAIL_LA} was not built" >&2
|
||||
exit 1
|
||||
fi
|
||||
if grep -q "^dlname=''" "$UNLINKFAIL_LA"; then
|
||||
echo "resume-recovery: static-only build, skipping the interposed leg"
|
||||
exit 0
|
||||
fi
|
||||
if [ ! -r "${UNLINKFAIL_LIB:-}" ]; then
|
||||
echo "resume-recovery: ${UNLINKFAIL_LIB:-\$UNLINKFAIL_LIB} was not built" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '[partial is closed before it is unlinked] ..\t'
|
||||
out="${tmpdir}/nounlink"
|
||||
mkdir "$out"
|
||||
interrupt_pass1 "$out" crange206mem --retries=1
|
||||
attempts="${tmpdir}/attempts.txt"
|
||||
: >"$attempts"
|
||||
# An LD_PRELOAD library loads ahead of the executable's own libasan, which ASan
|
||||
# refuses by default. The shim allocates nothing, so the ordering is harmless.
|
||||
export ASAN_OPTIONS="${ASAN_OPTIONS:+$ASAN_OPTIONS:}verify_asan_link_order=0"
|
||||
export UNLINKFAIL_MATCH=blob.bin UNLINKFAIL_LOG="$attempts" LD_PRELOAD="$UNLINKFAIL_LIB"
|
||||
rc=0
|
||||
run_with_timeout 120 httrack -O "$out" --quiet --disable-security-limits \
|
||||
--robots=0 --timeout=30 -c1 --retries=1 --continue \
|
||||
"${base}/crange206mem/index.html" >"${out}.log2" 2>&1 || rc=$?
|
||||
unset LD_PRELOAD
|
||||
test "$rc" -ne 124 || {
|
||||
echo "FAIL: the crawl never terminated"
|
||||
exit 1
|
||||
}
|
||||
# The shim silently matching nothing would make this leg a copy of test 48.
|
||||
seen=$(wc -l <"$attempts")
|
||||
test "$seen" -ge 1 || {
|
||||
echo "FAIL: the interposer saw no unlink of the partial; it never fired"
|
||||
exit 1
|
||||
}
|
||||
held=$(grep -c '^open ' <"$attempts" || true)
|
||||
test "$held" -eq 0 || {
|
||||
echo "FAIL: ${held} of ${seen} unlink(s) came while the handle was open"
|
||||
exit 1
|
||||
}
|
||||
got=$(blob_size "$out")
|
||||
test "$got" -eq "$crange_full" || {
|
||||
echo "FAIL: blob.bin is ${got} bytes, expected ${crange_full} (refetch lost)"
|
||||
exit 1
|
||||
}
|
||||
echo "OK (${got} bytes, ${seen} unlink(s), none on an open handle)"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user