19 Commits

Author SHA1 Message Date
Xavier Roche
c7767990cd ProxyTrack dumps a coucal hashtable stats line on every WebDAV request (#924)
* ProxyTrack dumps a coucal hashtable stats line on every WebDAV request

coucal logs a per-table statistics summary when a table is deleted, and
with no handler installed it prints that line itself, prefixed with the
table's address. ProxyTrack builds and drops one table per WebDAV
enumeration, so every unauthenticated PROPFIND put one on whatever the
service redirects its output to, heap pointer included.

httrack and htsserver were already covered by hts_init(), which installs
a global coucal handler that drops info-level messages unless HTS_LOG is
set (#416). ProxyTrack does not link libhttrack: it compiles coucal
itself and never calls hts_init(), so it was the last binary on coucal's
built-in sink. It now installs its own handler. Critical and warning go
through proxytrack's log, everything below is dropped unless HTS_LOG is
set, and the summaries then come back without the address.

Unnaming the tables would not have fixed it. coucal_delete() logs the
summary for every table, named or not; the name only decorates the
message.

Closes #918

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Whitelist proxytrack's request-time output instead of naming two absent strings

The absence check pinned two literals, so a reworded leak or one whose
pointer was not at column 0 walked through it. Assert instead that every
line the process writes once serving is an access-log line, which is the
property, and pin the routed form under HTS_LOG whole so the address
cannot creep back between the severity and the message.

Also say why the handler passes "debug" rather than the DEBUG macro: the
macro is NULL outside a debug build, which would quietly make HTS_LOG a
no-op.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Order the whitelist against a request's teardown, not just its access log

proxytrack writes the access line before send(), so waiting on it ordered
nothing that a teardown or keep-alive path writes afterwards: a leak
delayed 400ms past the response survived the check. Send both PROPFINDs
over one connection, since the keep-alive loop does not read the second
request until the first one's teardown has run, and read the capture only
once proxytrack is gone and the pty drainer has marked it complete.

Draining alone was not enough. SIGTERM cuts the work short rather than
truncating a buffer, so a still-pending write is never made at all and
there is nothing left to flush; the ordering is what catches it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 18:20:23 +00:00
Xavier Roche
0a1a4379c2 configure hangs forever if BASH_SHELL points at a FIFO (#926)
A FIFO passes "test -x", so the #920 check ran it: bash gets EACCES from
execve, falls back to reading the file for a shebang, and blocks in open()
with no writer. AS_EXECUTABLE_P is autoconf's own "test -f && test -x", and
AC_PATH_PROGS on the line above already applied it to the PATH search, so the
override path was simply using the weaker predicate.

A regular executable that never returns stays uncovered: no portable timeout
is worth it, and CC= pointing at the same wrapper hangs stock AC_PROG_CC too.

Test 151 gains the FIFO case, and its run() is capped so a regression fails
instead of wedging "make check" with no log.

Closes #922

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 20:16:12 +02:00
Xavier Roche
9479913232 http_xfread1's reserve-only read mode has no caller (#925)
* http_xfread1's reserve-only read mode has no caller

The `bufl == -2` branch of `http_xfread1()` allocates the line buffer and
returns without reading. Nothing has ever called it: no call site in the
tree passes -2, and scanning all 2649 revisions in this repository for
`xfread1(` call sites turns up 24 distinct lines, none of them -2. The
branch arrived with the 3.20.2 import commented "force reserve", so it was
probably meant for a preallocate-then-fill pattern that never landed.
Naming it `HTS_XFREAD_RESERVE` in #919 made it read as a supported mode.

No external caller is possible either: `htslib.h` is not installed and the
symbol is hidden, so this is not an API change.

Equivalence checked against the object code. `htsback.o` and
`htsselftest.o` disassemble identically; in `htslib.o` every function
except `http_xfread1` differs only in the `__LINE__` values `htssafe.h`
bakes in, shifted by the seven deleted lines. No new test: nothing changes
for any input a caller can produce, and `01_engine-xfread` plus the chunked
tests still pass.

Closes #923

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Say that any non-positive bufl is line mode, not just the two named

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0199wAkSVZNBNp51mpRkxMvv
Signed-off-by: Xavier Roche <roche@httrack.com>

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 18:04:31 +00:00
Xavier Roche
8168aa3a45 The masthead wordmark is a 400x34 GIF that blurs on any hi-DPI screen (#916)
* The masthead wordmark is a 400x34 GIF that blurs on any hi-DPI screen

Replaces it with an outlined SVG across the 38 documentation and WebHTTrack
pages that carry it. The original was set in Futura, so the lockup was refitted
in Jost*, the closest free Futura revival, taking weight from the measured stem
thickness, size from the cap heights and tracking by least squares against the
glyph positions in the old bitmap.

tests/82 now asserts that every image a GUI page names is actually served.

Signed-off-by: Xavier Roche <xroche@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Point the shared chrome generator at the new wordmark

The masthead of the 13 generated pages comes from tools/doc-chrome.py, so
editing the pages alone left the generator disagreeing with its own output and
--check red.

Signed-off-by: Xavier Roche <xroche@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

---------

Signed-off-by: Xavier Roche <xroche@gmail.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 19:17:01 +02:00
Xavier Roche
72fa732f7e A debug fprintf dumps every PROPFIND body to stderr (#917)
* proxytrack: drop the leftover debug traces on the WebDAV path

Two fprintf(stderr) calls in the PROPFIND path shipped by accident: one dumped
the client-supplied request body, the other the whole generated multistatus
response. Both ran on every PROPFIND with no authentication in front of them,
so any client could write bytes of its choosing into the operator's stderr.
The body is never parsed and the response is derivable from the index, so
neither trace has diagnostic value worth keeping behind a debug level.

Closes #911

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* tests: make the #911 leak check see stdout and the property, not two literals

The absence checks pinned 'DEBUG: DAV-DATA' and '^RESPONSE:', which four
re-added variants walk straight past: a trace without the hyphen, one with no
marker at all, one prefixed so the '^' misses, and one on stdout. The stdout
case is the worst of them: redirected to a file, proxytrack's stdout is fully
buffered and SIGTERM never flushes it, so the leak never reached the log the
test reads.

Give proxytrack a pty instead of a file, so libc line-buffers its output on
Linux and macOS alike, and assert the property: a PROPFIND carries a canary the
index cannot produce, and neither the canary nor a distinctive string from the
generated response may appear in what proxytrack wrote. The two literals stay as
names for the specific regression. The liveness guard now requires a PROPFIND
answered 207, since a depth-rejected one is logged 403 by the shared reply path
without ever reaching the deleted code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 11:31:53 +00:00
Xavier Roche
5a38473343 A failed String allocation is only caught by assert, and the empty-on-failure contract is unstated (#921)
* Bound the ProxyTrack DAV item buffer against an amplified PROPFIND path

proxytrack_add_DAV_Item() reserved a fixed 1024 bytes and then sprintf'd into
it unbounded. The request path lands in the response twice, once as the href
and once as the displayname, and escapexml() turns each '&' into '&amp;', so
an unauthenticated PROPFIND of roughly 900 ampersands writes about 9000 bytes
off the end of the heap block. No cache entry and no Depth: 1 are needed.

Replace the hand-sized reserve with StringSprintf(), which measures the
formatted output and grows the String to fit, and convert the sibling sprintf
sites in the same file so no unbounded write into a String is left to
re-audit. Sizing beats clipping here: the String already owns a growable
buffer, so nothing has to be dropped.

Closes #836

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Bound StringSprintf's pre-C99 retry, and trim the review findings

A genuine vsnprintf conversion error returns -1 just as pre-C99 msvcrt does
for a short buffer, so the doubling search had no way to tell them apart and
grew until realloc aborted. Unreachable from these format strings, which use
only %s and %d, but the helper lives in a shared header and will get more
callers. Cap the search and empty the String past it.

Also: the count assertion piped into wc under pipefail, so a zero count killed
the test through set -e before its diagnostic could print.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Lift the NO_WEBDAV conditional out of a macro argument list

A preprocessor directive inside a macro invocation's arguments is undefined:
it was fine while this was a plain sprintf() call, and MSVC rejected it as
soon as it became StringSprintf(). GCC accepts it, so only the Windows leg
caught it. Compute the DAV header fragment first and pass it as an argument.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Cover StringSprintf's exact-fill case and the WebDAV enumeration branch

StringSprintf_ writes the terminator at buffer[ret], so widening its
`ret < capacity` guard by one byte is a heap overflow that only fires when the
formatted output exactly fills the capacity. No crawl test lands on a
capacity boundary, so the mutant survived the suite. The new `strsprintf`
self-test sweeps lengths around 256, 512, 1024 and 2048 with the String's
capacity pinned to each, plus a growing and shrinking sweep on one reused
String, and checks the length, the bytes and the terminator every time.

Test 147 only ever sent Depth: 0, leaving the enumeration branch the same PR
rewrote with no coverage at all. Its fixture gains a child directory, and a
Depth: 1 listing pins the item URLs, including the trailing '/' that
StringPopRight takes back off a directory name.

Signed-off-by: Xavier Roche <xroche@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Make the String failure paths safe without assert

StringSprintf empties the String when it gives up, but that contract was
only visible in the implementation, and the WebDAV enumeration in
proxytrack pops the trailing '/' straight after it. State it at the
declaration, no-op StringPopRight on an empty String, and skip an
enumerated item the formatter could not name.

StringRoomTotal reported a failed realloc through STRING_ASSERT alone.
The MSVC Release configuration defines NDEBUG, so that check is already
gone from the shipped Windows builds, leaving a NULL buffer under a
capacity bumped before the allocation was known to succeed. Assign both
only on success, and terminate through StringOom_.

Closes #915

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Renumber the String OOM test to 152

151 is taken by the unmerged tests/151_bash-shell-validate.test (PR #920).
The filenames differ, so git would have carried both onto master rather than
conflicting.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Declare the new WebDAV test in the Windows skip set

It skips on Windows for the same reason as its two neighbours, MSYS
cannot reap a background listener (#595), and the ratchet fails a skip
it was not told about.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0199wAkSVZNBNp51mpRkxMvv
Signed-off-by: Xavier Roche <roche@httrack.com>

* Keep the out-of-memory action overridable

STRING_REALLOC and STRING_FREE are #ifndef hooks, and STRING_ASSERT was one
too; replacing it with a hard-wired call took a hook away from downstreams of
this installed header. Route the failure through STRING_OOM instead, with the
print-and-abort default unchanged.

Also flush stderr before aborting: the Windows CRT buffers a redirected
stderr and abort() flushes nothing, which would drop the message the test
matches on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Inject the allocation failure instead of asking for a huge one

The engine self-test pinned a String's capacity so the next doubling asked
for SIZE_MAX/2, on the assumption that no allocator would serve it. Six CI
legs disagreed: i386 has a 3G user space, and the 64-bit runners handed the
request out too, so the test reported "NOT aborted" everywhere but here.
Green that depends on how much memory the machine feels like giving is not a
test.

Drive the path from a standalone helper instead, which defines STRING_REALLOC
to a stub returning NULL before including htsstrings.h. Four cases: growth
with the stub allocating for real, the failure reaching the handler with the
size it asked for, a live buffer surviving a failed realloc, and the shipped
handler printing and aborting. Only the automake build produces the helper,
so the test declares its Windows skip.

The self-test had no portable way to force the failure, so it goes rather
than staying as a handler nobody can rely on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Assert the bytes and the requested size, not just the bookkeeping

An under-allocation survived the helper: shortening the realloc by one byte
while still recording the full capacity left all four cases green, because
only the growth case allocated anything and it checked the capacity number
rather than the memory behind it. Fill the announced capacity to its last
byte and read it back, which the sanitizer legs turn into a hard failure.

The failure cases pinned the initial capacity by asserting 16, so bumping
that policy would have failed a correct tree. Compare the size handed to the
handler against the size the stub was actually asked for instead, which also
catches the under-allocation on legs with no sanitizer.

Drive StringSprintf_ and StringBuffN_ too, the other two places the header
expands STRING_OOM.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Signed-off-by: Xavier Roche <xroche@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 11:29:24 +00:00
Xavier Roche
6745ec9842 configure accepts a BASH_SHELL that is not a usable bash (#920)
* configure accepts a BASH_SHELL that is not a usable bash

`./configure BASH_SHELL=/bin/sh` was accepted without a word. `AC_PATH_PROGS`
takes any absolute value verbatim, so the macOS problem #895 fixed (a bash in
POSIX sh-mode driving `make deb` and the test harness) came back, surfacing
much later as a `146_bash-shell.test` failure instead of a configure error. A
relative value never reached the Makefiles at all: it was dropped for whatever
the PATH search turned up.

configure now checks the value it resolved. The shell must be executable,
report a `BASH_VERSION`, and not carry `posix` in `SHELLOPTS`, the
discriminator `146_bash-shell.test` already uses, since an sh-mode bash reports
a version too. A relative or whitespace-carrying override is refused before the
search runs; whitespace would otherwise survive into `$(BASH_SHELL)`, which
nothing in the Makefiles quotes.

Only an explicit override is fatal. When the search itself finds nothing
usable, configure warns and carries on, so a box without bash still builds; it
just cannot run `make check` or `make deb`.

Closes #908

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* An environment in POSIX mode must not be blamed on the bash path

POSIXLY_CORRECT, or an exported SHELLOPTS, puts every bash into POSIX
sh-mode, so `./configure BASH_SHELL=/bin/bash` failed with advice to pass a
path that cannot exist, and a plain configure warned that no usable bash was
found on a box that has one. The probe now runs a second time under `env -u
POSIXLY_CORRECT -u SHELLOPTS`; if the shell is fine once they are cleared, the
message names them and says how to clear them for make as well, since it
inherits the environment. An override stays fatal, the search still only warns.

The bash-ness probe read `BASH_VERSION`, an ordinary variable any shell echoes
back, so `BASH_VERSION=9.9 ./configure BASH_SHELL=/bin/dash` was accepted and
dash landed in `TEST_LOG_COMPILER`. It reads `${BASH_VERSINFO[0]}` instead,
which no environment can fake.

The path guard covered whitespace alone while claiming to cover what make and
the recipe shell split on, so a real bash under a directory named with `;` or
`$` or `#` still reached the Makefile. It now rejects that whole class. Quoting
`$(BASH_SHELL)` at its three uses was the alternative, but make cuts the value
at a `#` and expands a `$` before any shell sees it, so quoting would cover
less than the guard.

The suite now also pins the branch the fatal/warn split rests on: no override,
an unusable bash first in PATH, configure exits 0 with a warning.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Nothing pinned which cause of POSIX mode gets blamed

A shell can be in POSIX mode because it was invoked as sh or because the
environment forces it, and only the second probe can say which. Nothing held
that down, so a version deciding from the environment alone, without
re-probing, passed every case in the suite while telling the user to clear a
variable that would not have helped. Test 151 now runs a bash symlinked as sh
with POSIXLY_CORRECT=1 set as well, and requires the message to name the path.

The comment records why that branch cannot simply read POSIXLY_CORRECT:
autoconf runs "set -o posix" on configure's own shell, so it is set there no
matter what the user's environment holds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 11:08:48 +00:00
Xavier Roche
eb4c1fe64c http_xfread1's read mode is an unnamed integer at every call site (#919)
* A chunked response carrying trailers is discarded as "Invalid chunk"

The chunk automaton expected the line after the terminating zero-length
chunk to be empty. RFC 9112 7.1.2 lets a server put a trailer section
there and asks recipients to discard fields they do not understand;
instead the whole message failed and the resource never reached the
mirror.

The trailer section is now read the way headers are, as a block ending
on a blank line, and thrown away. Reading it as a block also bounds it:
trailers carry no length of their own, so an endless one would hold a
connection slot forever, and the line reader's 8KB buffer caps it. Only
the terminating chunk opens the section, so junk where a data chunk's
own CRLF belongs is still a framing error.

Closes #855

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0199wAkSVZNBNp51mpRkxMvv
Signed-off-by: Xavier Roche <roche@httrack.com>

* Renumber the trailer test to 149, 147 is taken by the WebDAV overflow test

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0199wAkSVZNBNp51mpRkxMvv
Signed-off-by: Xavier Roche <roche@httrack.com>

* Name the line-block bound, and pin the trailer edge cases

Review follow-ups: the 8190-byte cap that bounds a trailer section was an
unnamed literal inside http_xfread1, so raising it for large response
headers would have moved the trailer bound silently. It is now
HTS_LINE_BLOCK_SIZE, named where the reader is declared.

The trailer path also no longer runs the chunk-size parse it then
discards, and eof.html pins the deliberate leniency the change
introduces: past a complete, length-verified body, a trailer section cut
before its blank line still lands. A body cut before the terminating
chunk stays refused.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0199wAkSVZNBNp51mpRkxMvv
Signed-off-by: Xavier Roche <roche@httrack.com>

* Name http_xfread1's read modes instead of passing bare 0, -1 and -2

http_xfread1() selects its read mode from the sign of bufl: a positive
value reads that many bytes, 0 stops at a blank line, -1 at the first LF,
-2 only reserves the buffer. Nothing declared them, so every call site was
an unexplained literal.

Declare HTS_XFREAD_LINE_BLOCK, HTS_XFREAD_LINE and HTS_XFREAD_RESERVE in
htslib.h beside HTS_LINE_BLOCK_SIZE, and use them at each call site. The
selftest keeps its 8192, a byte count rather than a mode.

Behaviour-preserving: the preprocessed output of the changed translation
units is token-identical once the parens around the negative literals and
the __LINE__ digits shifted by the reworded comments are normalized away.

Closes #914

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Note that the reserve-only read mode has no caller

Naming it made it read as a supported mode; it is unreachable (#923).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0199wAkSVZNBNp51mpRkxMvv
Signed-off-by: Xavier Roche <roche@httrack.com>

* Keep the reserve-mode note on one line

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0199wAkSVZNBNp51mpRkxMvv
Signed-off-by: Xavier Roche <roche@httrack.com>

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 10:53:02 +00:00
Xavier Roche
2d041866ed An unauthenticated PROPFIND overflows the ProxyTrack DAV item buffer (#909)
* Bound the ProxyTrack DAV item buffer against an amplified PROPFIND path

proxytrack_add_DAV_Item() reserved a fixed 1024 bytes and then sprintf'd into
it unbounded. The request path lands in the response twice, once as the href
and once as the displayname, and escapexml() turns each '&' into '&amp;', so
an unauthenticated PROPFIND of roughly 900 ampersands writes about 9000 bytes
off the end of the heap block. No cache entry and no Depth: 1 are needed.

Replace the hand-sized reserve with StringSprintf(), which measures the
formatted output and grows the String to fit, and convert the sibling sprintf
sites in the same file so no unbounded write into a String is left to
re-audit. Sizing beats clipping here: the String already owns a growable
buffer, so nothing has to be dropped.

Closes #836

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Bound StringSprintf's pre-C99 retry, and trim the review findings

A genuine vsnprintf conversion error returns -1 just as pre-C99 msvcrt does
for a short buffer, so the doubling search had no way to tell them apart and
grew until realloc aborted. Unreachable from these format strings, which use
only %s and %d, but the helper lives in a shared header and will get more
callers. Cap the search and empty the String past it.

Also: the count assertion piped into wc under pipefail, so a zero count killed
the test through set -e before its diagnostic could print.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Lift the NO_WEBDAV conditional out of a macro argument list

A preprocessor directive inside a macro invocation's arguments is undefined:
it was fine while this was a plain sprintf() call, and MSVC rejected it as
soon as it became StringSprintf(). GCC accepts it, so only the Windows leg
caught it. Compute the DAV header fragment first and pass it as an argument.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Cover StringSprintf's exact-fill case and the WebDAV enumeration branch

StringSprintf_ writes the terminator at buffer[ret], so widening its
`ret < capacity` guard by one byte is a heap overflow that only fires when the
formatted output exactly fills the capacity. No crawl test lands on a
capacity boundary, so the mutant survived the suite. The new `strsprintf`
self-test sweeps lengths around 256, 512, 1024 and 2048 with the String's
capacity pinned to each, plus a growing and shrinking sweep on one reused
String, and checks the length, the bytes and the terminator every time.

Test 147 only ever sent Depth: 0, leaving the enumeration branch the same PR
rewrote with no coverage at all. Its fixture gains a child directory, and a
Depth: 1 listing pins the item URLs, including the trailing '/' that
StringPopRight takes back off a directory name.

Signed-off-by: Xavier Roche <xroche@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Declare the new WebDAV test in the Windows skip set

It skips on Windows for the same reason as its two neighbours, MSYS
cannot reap a background listener (#595), and the ratchet fails a skip
it was not told about.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0199wAkSVZNBNp51mpRkxMvv
Signed-off-by: Xavier Roche <roche@httrack.com>

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Signed-off-by: Xavier Roche <xroche@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 10:37:22 +00:00
Xavier Roche
5f52c3d942 A chunked response carrying trailers is discarded as "Invalid chunk" (#913)
* A chunked response carrying trailers is discarded as "Invalid chunk"

The chunk automaton expected the line after the terminating zero-length
chunk to be empty. RFC 9112 7.1.2 lets a server put a trailer section
there and asks recipients to discard fields they do not understand;
instead the whole message failed and the resource never reached the
mirror.

The trailer section is now read the way headers are, as a block ending
on a blank line, and thrown away. Reading it as a block also bounds it:
trailers carry no length of their own, so an endless one would hold a
connection slot forever, and the line reader's 8KB buffer caps it. Only
the terminating chunk opens the section, so junk where a data chunk's
own CRLF belongs is still a framing error.

Closes #855

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0199wAkSVZNBNp51mpRkxMvv
Signed-off-by: Xavier Roche <roche@httrack.com>

* Renumber the trailer test to 149, 147 is taken by the WebDAV overflow test

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0199wAkSVZNBNp51mpRkxMvv
Signed-off-by: Xavier Roche <roche@httrack.com>

* Name the line-block bound, and pin the trailer edge cases

Review follow-ups: the 8190-byte cap that bounds a trailer section was an
unnamed literal inside http_xfread1, so raising it for large response
headers would have moved the trailer bound silently. It is now
HTS_LINE_BLOCK_SIZE, named where the reader is declared.

The trailer path also no longer runs the chunk-size parse it then
discards, and eof.html pins the deliberate leniency the change
introduces: past a complete, length-verified body, a trailer section cut
before its blank line still lands. A body cut before the terminating
chunk stays refused.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0199wAkSVZNBNp51mpRkxMvv
Signed-off-by: Xavier Roche <roche@httrack.com>

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 11:56:22 +02:00
Xavier Roche
592f3dd154 The frozen-slot spool is written inside the mirror namespace (#912)
* Spool a frozen backlog slot outside the mirror namespace

back_cleanup_background() named the spool file by appending ".tmp" to the save
name, so it landed beside the mirrored file. That is the shape #774 fixed for
the re-fetch backup: a site serving <path>.tmp has its mirrored copy truncated
by filecreate() and then unlinked when the slot is woken, and the run still
reports success. It is reachable on defaults, not only under a saturated
backlog: a -Z crawl of the bundled bigcrawl site with -c4 logs slots moving to
background.

Route both name shapes through back_spoolname(), which puts them in the
~hts-tmp directory no save name can spell, and drop that directory at the two
sites that unlink a spool. Left out of the #774 PR because these lines also
carried the overflow in #857.

Closes #859

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Keep the -p0 spool relative when no output directory is set

The new name inserted its own separator before ~hts-tmp, but path_html_utf8
already carries one and is empty when -O is absent, so the spool became
/~hts-tmp/tmpfile0.tmp: absolute, in the filesystem root. Master built
"%stmpfile%d.tmp" and stayed relative to the working directory.

create_back_tmpfile() has spelled it the same way since #842. Its empty
path branch looks unreachable from the three call sites, so this side is a
consistency fix with no test behind it, unlike the spool.

The self-test pinned the doubled slash it observed rather than the shape it
wanted; it now asserts the single-separator form and covers the empty
path_html_utf8 case that produced the root path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 08:44:06 +00:00
Xavier Roche
620418a9c6 The rings background is a 501x456 GIF that blurs on any hi-DPI screen (#910)
The panel decoration was drawn as vector and flattened to a 4KB indexed GIF
some time around 2007, with the panel colour baked in as an opaque backdrop.
Refitting its four ellipse boundaries recovers the original geometry, so it
goes back to being what it was: two elliptical annuli, 553 bytes of SVG, with
a transparent background that now composites over the panel instead of having
to match it.

Rasterised at the same size, the only pixels that differ from the GIF are
single-pixel anti-aliasing fringes along the four boundaries. Nothing survives
a 3x3 erosion of that difference, so no edge has moved.

Dark mode still drops the image rather than inverting it, since the ring
lavender is a light-panel tone whichever way the file stores it.

The engine keeps its own embedded copy of this GIF for the backblue.gif it
writes into mirrors. That one is untouched: the filename and byte length are
a contract with pages already on disk.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 08:24:21 +00:00
Xavier Roche
f3fa3a8b98 configure discards a user-supplied BASH_SHELL (#907)
* configure discards a user-supplied BASH_SHELL

AS_UNSET erased the variable before AC_PATH_PROGS could honour it, so
"./configure BASH_SHELL=/path" had no effect and there was no way to
point the build at a bash other than the first one on PATH. Nothing
presets BASH_SHELL, which was the whole problem with BASH in #895, so
declaring it precious is enough.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Run the nested configure against a symlink farm

An in-tree build leaves a config.status in srcdir, and autoconf then refuses
the out-of-tree run the test needs. Every CI build leg builds in-tree, so the
check failed there while passing on an out-of-tree tree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Read the resolved bash from the configure trace, not the Makefile

The nested configure ran without the flags the outer one was given, so on
macOS it died at the openssl check that Homebrew paths satisfy. BASH_SHELL
is resolved long before that, so assert on the trace and let the run fail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Assert the value that reaches $(BASH_SHELL), not the macro's decision

Reading the configure trace let a mutant through: resolve the override
correctly, clobber BASH_SHELL one line later, and both assertions passed
while every Makefile got the wrong shell. Prefer the generated Makefile
and keep the trace only as a fallback for a configure that dies early.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 12:53:15 +02:00
Xavier Roche
311b99afd4 The AppStream metainfo still advertises WebHTTrack 3.49.8 (#897)
* The AppStream metainfo still advertises WebHTTrack 3.49.8

The metainfo installs to usr/share/metainfo, so GNOME Software and KDE
Discover read both the version and the "What's new" text out of it. Its
releases block held one entry, 3.49.8, and nothing had moved it since.

It now lists 3.49.8 through 3.49.15, newest first, each with a short
user-facing note taken from history.txt. Dates come from the git tags;
that also corrects 3.49.8's, which carried 3.49.7's date.

01_engine-version-macros.test gains two assertions so the next release
cannot miss this file, or configure.ac: AC_INIT and the top release entry
must both match HTTRACK_VERSIONID, and the entries must descend so the
top one really is the newest.

Closes #884

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Harden the metainfo version check and correct four release notes

The release-version extraction matched <releases ...> as well as <release>,
ignored XML comments and took only the last tag on a shared line, so a parked
or wrapper version= could pose as the newest entry and pass a stale metainfo.
Split tags one per line, drop comments, and anchor the match. Widen the awk
ordering key so a component of 1000 or more cannot borrow into the next.

In the notes: "3.49-2" and "site rules with wildcards" are unreadable in a
software centre, the Windows path bullet does not apply to the Unix WebHTTrack
GUI it ships with, and 3.49.15 listed no web-interface fix at all.

Signed-off-by: Xavier Roche <xroche@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Signed-off-by: Xavier Roche <xroche@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:02:00 +00:00
Xavier Roche
ef81f5b488 An install that is not where configure put it cannot find its own data (#899)
* Find the data directory instead of trusting the configure-time one

webhttrack probed a fixed list of prefixes that nothing derived from
--datadir, and the engine baked $(datadir) into the binary with the
argv[0] fallback compiled out. Both fail on any tree that is not where
it was configured.

configure substitutes the real datadir into src/webhttrack, and
hts_resolve_datadir() prefers the compiled-in path but derives one from
argv[0] when it is gone, so a moved install reads its own templates
rather than silently falling back to the built-in defaults.

Closes #887
Closes #894

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Resolve the data directory from the executable, not just argv[0]

MSVC broke: HTS_HTTRACKDIR is only defined on non-Windows, so the
argv[0] branch this replaced was live there, not dead. Windows now
passes an empty builtin and falls back to the executable's own
directory, which is what it did before -- except fconcat inserts no
separator, so the old path_bin lacked its trailing slash and never
resolved a template anyway.

Ask the OS for the executable path (/proc/self/exe, _NSGetExecutablePath,
GetModuleFileName) and keep argv[0] as the fallback, so a mirror run
through a PATH lookup resolves too.

The bundle drops the substituted datadir from its copy of webhttrack:
it is a build-machine path there, and the relative entries ahead of it
already find the payload.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Clip the candidate path instead of aborting on a long argv[0]

strlncatbuff() aborts rather than truncates, and appending the layout
suffix to an already-full buffer reaches that: a directory part within
17 bytes of the candidate buffer's size killed the process. Build the
candidate with snprintf and skip it when it does not fit.

The self-test now drives a directory part long enough to trigger it;
without the fix it aborts on "overflow while appending 'layout[i]'".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Ignore the generated src/webhttrack

An in-tree build writes it next to webhttrack.in, where it was untracked
and one "git add -A" away from re-entering the tree with a build
machine's datadir frozen into it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:43:50 +00:00
Xavier Roche
22c506944d configure resolves bash to /bin/sh on macOS, and make deb runs a bash script with dash (#898)
* configure resolves bash to /bin/sh on macOS, and make deb runs a bash script with dash

AC_PATH_PROGS searched into BASH, which bash presets to its own invocation
path. configure re-execs through /bin/sh, and on macOS that shell is a bash, so
the macro honoured the pre-set value and reported "checking for bash...
/bin/sh": a bash in sh-mode that rejects process substitution. Search into
BASH_SHELL instead, a name no shell presets, with AS_UNSET in front so the
environment cannot preset it either.

That makes the obvious fix for the deb target safe. It ran tools/mkdeb.sh with
$(SHELL), which is /bin/sh, so "make deb" died on the first bashism on every
Debian and Ubuntu box. macos-app.sh stays on $(SHELL): it is POSIX sh on
purpose, so shellcheck lints it as sh.

tests/146_bash-shell.test asserts the configured shell exists, sets
BASH_VERSION, is not in POSIX sh-mode, and parses a process substitution.

Closes #895
Closes #891

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Trim the comments this branch added, and drop a claim that is no longer true

The four comment blocks the branch added ran two to five lines where one or two
carry the fact. The 146 header also said the macOS shell rejects "the process
substitution the bundle script uses": tools/macos-app.sh has been POSIX sh with
no process substitution since #890, so the gate is there for tests/local-crawl.sh
and tests/webhttrack-smoke.sh, which is what the comment now says.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 07:05:44 +00:00
Xavier Roche
b127323a68 The macOS test server stalls 35s on a reverse DNS lookup nothing reads (#896)
* The test server's bind reverse-resolves 127.0.0.1, which stalls on macOS

http.server's HTTPServer.server_bind() calls getfqdn() on the bind address just
to fill server_name, which nothing in local-server.py reads. On the macos-15
runner that lookup takes ~30s, so the PORT line lands well past every caller's
discovery budget (#870).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* PROBE: macos-15 + startup timing (not for merge)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Move the macOS CI legs to macos-15, and route the last two port waits through the shared helper

84 and 100 kept their own PORT poll loops with 10s budgets, the copies #869 did
not reach; both use discover_server_port now, so there is one implementation
left.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Cut the server_bind comment to the why

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 06:21:39 +00:00
Xavier Roche
fac60dff4a armhf crash reports have no frames, and the empty backtrace fails the build (#893)
* armhf crash reports have no frames, and the empty backtrace fails the build

gcc emits no unwind tables on armhf, so backtrace() comes back empty and the
handler printed a frameless report, which the crash tests read as a failure and
which left 3.49.15-1 stuck at Build-Attempted there. Ask for
-fasynchronous-unwind-tables where the compiler takes it, and say why the report
has no frames when the unwinder still returns nothing.

Closes #892

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Let ASan tolerate the backtrace shim's link order

An LD_PRELOAD library loads ahead of the executable's own libasan, which ASan
refuses by default, so the sanitize leg never reached the crash it was meant to
inspect. Same waiver the other interposer tests carry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Keep an empty backtrace a failure everywhere but 32-bit ARM

Relaxing test 80's skip to the shared message prefix let it swallow the new
"unwinding failed" wording too, so a build that traced nothing anywhere would
have gone green on every architecture. Skip only where nothing can be done
about it: the OS-less case, and 32-bit ARM if its toolchain still refuses to
unwind. Test 143 now pins the exact wording rather than the prefix it shares
with the OS-less notice, and its control run skips the symbolizer it has no
reason to spawn.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Declare the new backtrace test's Windows skip

The Windows job compares the skip set exactly, so a test that skips there for a
good reason still fails the gate until it is named.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 18:22:08 +00:00
Xavier Roche
264478c846 HTTrack ships a working GUI on macOS that no Mac user can find (#890)
* Ship a macOS app bundle

macOS users install HTTrack through Homebrew and get a working WebHTTrack they
are never told about: the formula installs webhttrack and htsserver, and the
only thing missing is something to double-click. tools/macos-app.sh assembles
HTTrack.app from an installed prefix, with the payload under Contents/Resources
so webhttrack keeps resolving htsserver and its data from its own location, and
a two-line stub in Contents/MacOS for Launch Services.

Nothing about the engine changes. The bundle is possible because webhttrack was
already relocatable and because the data symlink stopped being absolute (#885);
--disable-shared keeps libhttrack inside the binaries so nothing points back at
the staging prefix. That costs no crash diagnostics here, since backtraces are
gated on __linux (src/htsbacktrace.c:50), though it does trip #889 on Linux.

The script verifies what it builds rather than trusting it: no absolute symlink,
the served UI present, no Mach-O still linking the staging prefix, and the
Info.plist version matching the installed binary. configure generates that
plist, so it cannot drift into a fifth hand-maintained version spot of the kind
#884 describes.

CI assembles the bundle, runs the webhttrack smoke through the stub, then moves
the bundle and deletes the prefix it came from and runs it again, which is the
one thing a .app has to survive that a prefix install does not. An ad-hoc
codesign proves it is well formed enough to sign; Gatekeeper needs a Developer
ID and stays out of scope with the DMG.

No custom icon: the largest artwork in the tree is 48x48 and macOS wants 1024,
so that needs a real source asset.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* ci: lint the new bundle script, and mark it executable

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Drive the bundle through a make target, and run it with bash

Adds a macos-app target so assembling the bundle goes through the build system
the way make deb does, rather than CI reaching for the script directly.

It runs the script with $(BASH), not $(SHELL). automake's SHELL is /bin/sh, and
the script uses process substitution, so under dash it died partway: the payload
was already copied by then and only the verification was skipped, leaving a
bundle that looked built and had been checked by nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Make the bundle checks catch what they were missing

The smoke put the bundle's own bin on $PATH, so a launcher that ignored its own
location and just ran "webhttrack" passed as readily as the real one, which is
the most likely way a stub breaks. Dropping $prefix/bin from $PATH kills that:
the browser stub is found through webhttrack's SRCHPATH, not $PATH, so nothing
else needed it.

The bundle also shipped libtool .la files, static archives and include/, none of
them loadable from a static build and the .la files carrying the staging prefix
in libdir=. They are pruned, and a text sweep now fails on any remaining file
that embeds that prefix, which otool cannot see because it reads load commands
only. The prefix is resolved to an absolute path first, or a relative --prefix
made that grep match nothing.

Also: the symlink scan asserts it scanned something, and CFBundleVersion is
compared as well as CFBundleShortVersionString.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Assemble the bundle with POSIX sh, not $(BASH)

$(BASH) is not a reliable bash on macOS. /bin/sh there is bash in sh-mode, which
presets $BASH to the path it was invoked as, and AC_PATH_PROGS honours a
pre-set value rather than searching, so configure reports "checking for bash...
/bin/sh". That shell rejects process substitution, and the bundle job died on
it.

Rather than hunt for a real bash, the script no longer needs one: the three
process substitutions become temp-file loops, pipefail goes (not POSIX), and
the target is back on the ordinary $(SHELL) like deb:. shellcheck now reads it
as sh, so a bashism creeping back fails lint instead of macOS CI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 20:05:20 +02:00
104 changed files with 2701 additions and 358 deletions

View File

@@ -176,18 +176,6 @@ jobs:
sudo ifconfig lo0 alias 127.0.0.2 up
sudo ifconfig lo0 alias 127.0.0.3 up
- name: PROBE runner and sample(1) capability
run: |
set +e
sw_vers; echo "ncpu=$(sysctl -n hw.ncpu) mem=$(sysctl -n hw.memsize)"
sleep 120 &
victim=$!
/usr/bin/sample "$victim" 2 -mayDie >/tmp/sample.out 2>/tmp/sample.err
echo "sample rc=$?"
echo "--- has Call graph: ---"; grep -c '^Call graph:' /tmp/sample.out
head -20 /tmp/sample.out; echo "--- stderr ---"; head -20 /tmp/sample.err
kill $victim 2>/dev/null
- name: Test
timeout-minutes: 20
run: |
@@ -198,17 +186,6 @@ jobs:
if: failure()
run: cat tests/test-suite.log 2>/dev/null || true
- name: PROBE dump every failing test's own log
if: failure()
run: |
set +e
for trs in tests/*.trs; do
grep -q '^:test-result: \(FAIL\|ERROR\)' "$trs" || continue
log="${trs%.trs}.log"
echo "########## $log ##########"
cat "$log"
done
# Runtime smoke of the WebHTTrack launcher on macOS: it carries a Darwin-only
# browser path (open -W) and nothing else exercises htsserver. Install into a
# temp prefix, then check webhttrack brings up htsserver and serves the UI.
@@ -270,6 +247,65 @@ jobs:
- name: Smoke-test webhttrack
run: bash tests/webhttrack-smoke.sh "$RUNNER_TEMP/inst"
# The macOS app bundle (#886): assemble it, then prove it still works after being
# moved, which is the only thing a .app has to survive that a prefix install does not.
macos-app:
name: macOS app bundle (arm64)
runs-on: macos-15
steps:
- uses: actions/checkout@v7
with:
submodules: recursive
- name: Install build dependencies
run: |
set -euo pipefail
brew install autoconf automake libtool autoconf-archive brotli zstd
# --disable-shared keeps libhttrack inside the binaries, so the bundle carries no
# absolute dylib path back to the staging prefix.
- name: Build and install into a temp prefix
run: |
set -euo pipefail
ssl="$(brew --prefix openssl@3)"
brewp="$(brew --prefix)"
./bootstrap
./configure CPPFLAGS="-I${ssl}/include -I${brewp}/include" \
LDFLAGS="-L${ssl}/lib -L${brewp}/lib" \
--prefix="$RUNNER_TEMP/inst" --disable-shared
make -j"$(sysctl -n hw.ncpu)"
make install
- name: Assemble HTTrack.app
run: |
set -euo pipefail
make macos-app APP_FLAGS="--prefix $RUNNER_TEMP/inst --out $RUNNER_TEMP"
plutil -lint "$RUNNER_TEMP/HTTrack.app/Contents/Info.plist"
- name: Run it through the bundle stub
run: |
set -euo pipefail
app="$RUNNER_TEMP/HTTrack.app"
bash tests/webhttrack-smoke.sh "$app/Contents/Resources" "$app/Contents/MacOS/HTTrack"
- name: Run it again after moving it
run: |
set -euo pipefail
mkdir -p "$RUNNER_TEMP/moved"
mv "$RUNNER_TEMP/HTTrack.app" "$RUNNER_TEMP/moved/"
rm -rf "$RUNNER_TEMP/inst"
app="$RUNNER_TEMP/moved/HTTrack.app"
bash tests/webhttrack-smoke.sh "$app/Contents/Resources" "$app/Contents/MacOS/HTTrack"
# No Developer ID here, so Gatekeeper cannot be exercised; an ad-hoc signature
# still proves the bundle is well formed enough to sign.
- name: Check the bundle is signable
run: |
set -euo pipefail
app="$RUNNER_TEMP/moved/HTTrack.app"
codesign -s - --force --deep "$app"
codesign --verify --strict --verbose=2 "$app"
# Portability/hardening: 32-bit (i386) build on the x86-64 runner via multilib
# -- no extra hardware. Exercises the 32-bit size_t/pointer ABI, where size
# and bounds math can truncate or wrap in ways 64-bit never reveals (the axis
@@ -646,9 +682,10 @@ jobs:
man/makeman.sh
src/htsbasiccharsets.sh
src/htsentities.sh
src/webhttrack
src/webhttrack.in
tests/*.sh
tests/*.test
tools/macos-app.sh
tools/mkdeb.sh
steps:
- uses: actions/checkout@v7

View File

@@ -265,15 +265,21 @@ jobs:
# cause a false mismatch either.
# footer-overflow and purge-longpath skip on Windows (need a path past MAX_PATH);
# crange pending #581;
# webdav-default and webdav-mime need a reapable background listener, which MSYS cannot give them;
# webdav-default, webdav-mime and webdav-overflow need a reapable background listener, which MSYS cannot give them;
# badmtime needs a filesystem that stores an mtime past gmtime's range;
# single-file ends on a GUI half needing htsserver, which this job does not build;
# update-304-leak needs a LeakSanitizer build, which MSVC has no equivalent of;
# crash-symbolize needs backtrace(), which Windows has no equivalent of.
# crash-symbolize and backtrace-empty need backtrace(), which Windows has no
# equivalent of;
# string-oom drives a helper binary that only the automake build produces.
expected_skips="01_engine-footer-overflow.test
100_local-purge-longpath.test
114_local-update-304-leak.test
120_local-proxytrack-webdav-default.test
143_engine-backtrace-empty.test
147_local-proxytrack-webdav-overflow.test
152_engine-string-oom.test
153_local-proxytrack-quiet.test
48_local-crange-memresume.test
71_local-crange-repaircache.test
79_local-proxytrack-webdav-mime.test

2
.gitignore vendored
View File

@@ -24,6 +24,8 @@ Makefile.in
/config.log
/config.status
/stamp-h1
# src/webhttrack.in is the source; an in-tree build generates this one (#887).
/src/webhttrack
Makefile
.deps/
.libs/

View File

@@ -6,13 +6,21 @@ ACLOCAL_AMFLAGS = -I m4
EXTRA_DIST = INSTALL.Linux \
gpl-fr.txt license.txt greetings.txt history.txt \
httrack-doc.html lang.def README.md tools/mkdeb.sh \
tools/macos-app.sh tools/Info.plist.in \
bootstrap build.sh
# Build the signed Debian packages from a clean source export. Pass the signing
# key and other options through DEB_FLAGS, e.g.:
# make deb DEB_FLAGS="--key BB71C7E6CB1AD8FAF53FE42A60C3AA7180598EFB"
# See tools/mkdeb.sh --help for all options.
# See tools/mkdeb.sh --help for all options. Not $(SHELL): mkdeb.sh is bash, and dash chokes (#891).
DEB_FLAGS =
deb:
$(SHELL) $(top_srcdir)/tools/mkdeb.sh $(DEB_FLAGS)
$(BASH_SHELL) $(top_srcdir)/tools/mkdeb.sh $(DEB_FLAGS)
.PHONY: deb
# Assemble the macOS application bundle from an installed prefix, e.g.
# make macos-app APP_FLAGS="--prefix /tmp/inst --out /tmp"
APP_FLAGS =
macos-app:
$(SHELL) $(top_srcdir)/tools/macos-app.sh --plist tools/Info.plist $(APP_FLAGS)
.PHONY: macos-app

View File

@@ -38,6 +38,70 @@ VERSION_INFO="3:7:0"
AM_MAINTAINER_MODE
AC_USE_SYSTEM_EXTENSIONS
# A real bash, for "make deb" and the test harness. Not searched into BASH: bash presets
# that to its own path, and macOS /bin/sh is a bash, so the macro never searches (#895).
# BASH_SHELL isn't preset the way BASH is, so AC_ARG_VAR needs no guard. Kept ahead of the
# compiler probes so a bad override dies before them.
AC_ARG_VAR([BASH_SHELL], [path to a real (non-POSIX-mode) bash])
# AC_PATH_PROGS drops a relative override and searches instead, which loses the user's intent.
# Nothing quotes $(BASH_SHELL) in the Makefiles, and quoting could not save it anyway: make
# splits on whitespace, expands '$' and treats '#' as a comment before any shell sees it.
case $BASH_SHELL in
*[[[:space:]]]* | *'#'* | *'$'* | *'`'* | *'\'* | *'"'* | *"'"* | *';'* | *'&'* | *'|'* | \
*'<'* | *'>'* | *'('* | *')'* | *'*'* | *'?'* | *'@<:@'* | *'@:>@'* | *'{'* | *'}'*)
AC_MSG_ERROR([BASH_SHELL must not contain shell or make metacharacters, got: $BASH_SHELL]) ;;
'' | [[\\/]]* | ?:[[\\/]]*) ;;
*) AC_MSG_ERROR([BASH_SHELL must be an absolute path, got: $BASH_SHELL]) ;;
esac
hts_bash_override=$BASH_SHELL
AC_PATH_PROGS([BASH_SHELL], [bash], [/bin/bash])
# An absolute override is taken verbatim, so BASH_SHELL=/bin/sh would put #895 back and only
# surface at "make check" or "make deb" (#908). What we found ourselves is only a warning:
# a box with no bash still builds, it just cannot run those two.
AC_MSG_CHECKING([whether $BASH_SHELL is a bash outside POSIX mode])
hts_bash_why=
hts_bash_env=no
# AS_EXECUTABLE_P, not "test -x": the PATH search above already demands a regular file, and
# bash blocks forever reading a FIFO it failed to exec, so -x alone hangs configure (#922).
if ! AS_EXECUTABLE_P(["$BASH_SHELL"]); then
hts_bash_why="not an executable regular file"
elif test -z "$("$BASH_SHELL" -c 'echo "${BASH_VERSINFO[[0]]}"' 2>/dev/null)"; then
# Not BASH_VERSION: that is an ordinary variable, so any shell echoes back a spoofed one.
hts_bash_why="not a bash: it reports no BASH_VERSINFO"
else
# sh-mode bash reports a version too, so only SHELLOPTS tells the two apart.
case $("$BASH_SHELL" -c 'echo ":$SHELLOPTS:"' 2>/dev/null) in
*:posix:*)
hts_bash_why="a bash in POSIX sh-mode"
# POSIXLY_CORRECT and an exported SHELLOPTS do that to every bash on the box, so no path
# can pass and blaming this one would send the user hunting for another. Reading them
# here would not do: configure puts its own shell in posix mode, which sets both.
case $(env -u POSIXLY_CORRECT -u SHELLOPTS "$BASH_SHELL" -c 'echo ":$SHELLOPTS:"' 2>/dev/null) in
'' | *:posix:*) ;; # no "env -u", or posix whatever the environment: blame the path
*) hts_bash_env=yes ;;
esac
;;
esac
fi
if test -z "$hts_bash_why"; then
AC_MSG_RESULT([yes])
else
AC_MSG_RESULT([no])
hts_bash_msg="POSIXLY_CORRECT or SHELLOPTS forces every bash into POSIX sh-mode, $BASH_SHELL included. Clear them for configure and for make, which hands them to make check and make deb: env -u POSIXLY_CORRECT -u SHELLOPTS ..."
if test "$hts_bash_env" = yes; then
if test -n "$hts_bash_override"; then
AC_MSG_ERROR([$hts_bash_msg])
fi
AC_MSG_WARN([$hts_bash_msg])
else
if test -n "$hts_bash_override"; then
AC_MSG_ERROR([BASH_SHELL=$BASH_SHELL is $hts_bash_why])
fi
AC_MSG_WARN([no usable bash found: $BASH_SHELL is $hts_bash_why. "make check" and "make deb" need one; pass BASH_SHELL=/path/to/bash])
fi
fi
AC_PROG_CC
AM_PROG_CC_C_O
m4_warn([obsolete],
@@ -48,14 +112,13 @@ m4_warn([obsolete],
# script's behavior did not change. They are probably safe to remove.
AC_CHECK_INCLUDES_DEFAULT
AC_PROG_EGREP
# $(SED) substitutes $(datadir) into src/webhttrack
AC_PROG_SED
LT_INIT
AC_PROG_LN_S
LT_INIT
# bash, used to run the test scripts (see tests/Makefile.am TEST_LOG_COMPILER)
AC_PATH_PROGS([BASH], [bash], [/bin/bash])
# Export LD_LIBRARY_PATH name or equivalent.
AC_SUBST(SHLIBPATH_VAR,$shlibpath_var)
@@ -100,6 +163,9 @@ AX_CHECK_COMPILE_FLAG([-fstack-protector-strong], [DEFAULT_CFLAGS="$DEFAULT_CFLA
[AX_CHECK_COMPILE_FLAG([-fstack-protector], [DEFAULT_CFLAGS="$DEFAULT_CFLAGS -fstack-protector"], [], [-Werror])], [-Werror])
AX_CHECK_COMPILE_FLAG([-fstack-clash-protection], [DEFAULT_CFLAGS="$DEFAULT_CFLAGS -fstack-clash-protection"], [], [-Werror])
AX_CHECK_COMPILE_FLAG([-fcf-protection], [DEFAULT_CFLAGS="$DEFAULT_CFLAGS -fcf-protection"], [], [-Werror])
# backtrace() unwinds through these; armhf, unlike amd64/arm64, defaults them off
# and printed frameless crash reports.
AX_CHECK_COMPILE_FLAG([-fasynchronous-unwind-tables], [DEFAULT_CFLAGS="$DEFAULT_CFLAGS -fasynchronous-unwind-tables"], [], [-Werror])
# No --discard-all: it drops the local symbols naming every static function, so
# a trace misattributes them to the nearest surviving global. Costs 0.6% size.
AX_CHECK_LINK_FLAG([-Wl,--no-undefined], [DEFAULT_LDFLAGS="$DEFAULT_LDFLAGS -Wl,--no-undefined"])
@@ -360,5 +426,6 @@ html/Makefile
libtest/Makefile
tests/Makefile
fuzz/Makefile
tools/Info.plist
])
AC_OUTPUT

View File

@@ -128,7 +128,7 @@ This file lists all changes and fixes that have been made for HTTrack
+ Fixed: report why a -%L URL list could not be loaded (#49)
+ Changed: multiple internal hardening, build and CI improvements
.49-9
3.49-9
+ Fixed: file-type detection from the Content-Type header: trust a declared type over a binary URL extension, honor --assume under the delayed type check, and keep a known extension against a bogus or empty Content-Type (#267, #29, #56)
+ Fixed: an uninitialized-buffer read when the Content-Type is empty (#411)
+ Fixed: restored C++ source-compatibility of the installed headers so reverse dependencies (httraqt) build again (#413)

View File

@@ -16,7 +16,7 @@
<a class="skip" href="#main">Skip to content</a>
<header class="masthead">
<img src="images/header_title_4.gif" width="400" height="34" alt="HTTrack Website Copier">
<img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier">
<div class="tagline">Open Source offline browser</div>
</header>

View File

@@ -16,7 +16,7 @@
<a class="skip" href="#main">Skip to content</a>
<header class="masthead">
<img src="images/header_title_4.gif" width="400" height="34" alt="HTTrack Website Copier">
<img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier">
<div class="tagline">Open Source offline browser</div>
</header>

View File

@@ -16,7 +16,7 @@
<a class="skip" href="#main">Skip to content</a>
<header class="masthead">
<img src="images/header_title_4.gif" width="400" height="34" alt="HTTrack Website Copier">
<img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier">
<div class="tagline">Open Source offline browser</div>
</header>

View File

@@ -16,7 +16,7 @@
<a class="skip" href="#main">Skip to content</a>
<header class="masthead">
<img src="images/header_title_4.gif" width="400" height="34" alt="HTTrack Website Copier">
<img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier">
<div class="tagline">Open Source offline browser</div>
</header>

View File

@@ -26,7 +26,7 @@
<a class="skip" href="#main">Skip to content</a>
<header class="masthead">
<img src="images/header_title_4.gif" width="400" height="34" alt="HTTrack Website Copier">
<img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier">
<div class="tagline">Open Source offline browser</div>
</header>

View File

@@ -16,7 +16,7 @@
<a class="skip" href="#main">Skip to content</a>
<header class="masthead">
<img src="images/header_title_4.gif" width="400" height="34" alt="HTTrack Website Copier">
<img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier">
<div class="tagline">Open Source offline browser</div>
</header>

View File

@@ -72,14 +72,14 @@ body {
background: var(--panel);
border-bottom: 6px solid #000;
padding: 1.5rem;
/* The rings the 2007 pages carried. The image has the light panel colour baked
into it, so it is dropped rather than inverted in dark mode. */
background-image: url(images/bg_rings.gif);
/* The rings the 2007 pages carried. Transparent, so it rides on --panel. */
background-image: url(images/bg_rings.svg);
background-repeat: no-repeat;
background-position: top right;
}
@media (prefers-color-scheme: dark) {
/* Rings dropped, not inverted: their lavender is a light-panel tone. */
.wrap { background-image: none; }
/* The wordmark is black on transparent, and all but vanishes on the dark field. */

View File

@@ -16,7 +16,7 @@
<a class="skip" href="#main">Skip to content</a>
<header class="masthead">
<img src="images/header_title_4.gif" width="400" height="34" alt="HTTrack Website Copier">
<img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier">
<div class="tagline">Open Source offline browser</div>
</header>

View File

@@ -16,7 +16,7 @@
<a class="skip" href="#main">Skip to content</a>
<header class="masthead">
<img src="images/header_title_4.gif" width="400" height="34" alt="HTTrack Website Copier">
<img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier">
<div class="tagline">Open Source offline browser</div>
</header>

View File

@@ -16,7 +16,7 @@
<a class="skip" href="#main">Skip to content</a>
<header class="masthead">
<img src="images/header_title_4.gif" width="400" height="34" alt="HTTrack Website Copier">
<img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier">
<div class="tagline">Open Source offline browser</div>
</header>

View File

@@ -16,7 +16,7 @@
<a class="skip" href="#main">Skip to content</a>
<header class="masthead">
<img src="images/header_title_4.gif" width="400" height="34" alt="HTTrack Website Copier">
<img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier">
<div class="tagline">Open Source offline browser</div>
</header>
@@ -66,7 +66,7 @@ the screenshots and notes follow along.</p>
<div class="platforms" role="group" aria-label="Choose your version">
<button type="button" data-platform="win" aria-pressed="false">WinHTTrack <small>Windows</small></button>
<button type="button" data-platform="web" aria-pressed="false">WebHTTrack <small>Linux and Unix</small></button>
<button type="button" data-platform="web" aria-pressed="false">WebHTTrack <small>Linux, macOS and Unix</small></button>
<button type="button" data-platform="droid" aria-pressed="false">HTTrack <small>Android</small></button>
</div>
@@ -125,6 +125,11 @@ data-for="droid">Tap <b>Next</b></span> to create a project, or open one you alr
<p class="note" data-for="web">The language dropdown starts blank on purpose: its first entry
means "leave the interface as it is". Pick a language only if you want to change it.</p>
<p class="note" data-for="web">On macOS, <code>brew install httrack</code> installs WebHTTrack
alongside the command line tool. Open <b>HTTrack.app</b> if you have it, or run
<code>webhttrack</code> in a terminal; either way the interface opens in your browser and
behaves exactly as described here.</p>
<h2 id="step-project">2. Name the project</h2>
<p>A project is one mirror: its name becomes the folder your files land in, so give it something

View File

@@ -37,7 +37,7 @@ a:active { text-decoration: underline; }
border-bottom: 6px solid #000;
padding: 10px; padding-top: 20px;
line-height: 1.65em;
background-image: url(images/bg_rings.gif);
background-image: url(images/bg_rings.svg);
background-repeat: no-repeat;
background-position: top right;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

8
html/images/bg_rings.svg Normal file
View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="501" height="456" viewBox="0 0 501 456">
<title>HTTrack rings</title>
<!-- Transparent, unlike the GIF this replaces: the panel's own #ccd shows through. -->
<g fill="#b9b9d0" fill-rule="evenodd">
<path d="M614.59 346.3A323.36 197.55 21.16 1 0 11.46 112.89A323.36 197.55 21.16 1 0 614.59 346.3 ZM590.57 315.66A262.58 160.99 22.18 1 0 104.26 117.43A262.58 160.99 22.18 1 0 590.57 315.66 Z"/>
<path d="M578.08 292.16A185.6 113.57 21.23 1 0 232.08 157.73A185.6 113.57 21.23 1 0 578.08 292.16 ZM564.4 274.56A150.77 92.55 22.26 1 0 285.34 160.32A150.77 92.55 22.26 1 0 564.4 274.56 Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 641 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

1
html/images/wordmark.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@@ -13,7 +13,7 @@
<a class="skip" href="#main">Skip to content</a>
<header class="masthead">
<img src="images/header_title_4.gif" width="400" height="34" alt="HTTrack Website Copier">
<img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier">
<div class="tagline">Open Source offline browser</div>
</header>

View File

@@ -16,7 +16,7 @@
<a class="skip" href="#main">Skip to content</a>
<header class="masthead">
<img src="images/header_title_4.gif" width="400" height="34" alt="HTTrack Website Copier">
<img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier">
<div class="tagline">Open Source offline browser</div>
</header>

View File

@@ -16,7 +16,7 @@
<a class="skip" href="#main">Skip to content</a>
<header class="masthead">
<img src="images/header_title_4.gif" width="400" height="34" alt="HTTrack Website Copier">
<img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier">
<div class="tagline">Open Source offline browser</div>
</header>

View File

@@ -16,7 +16,7 @@
<a class="skip" href="#main">Skip to content</a>
<header class="masthead">
<img src="images/header_title_4.gif" width="400" height="34" alt="HTTrack Website Copier">
<img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier">
<div class="tagline">Open Source offline browser</div>
</header>

View File

@@ -16,7 +16,7 @@
<a class="skip" href="#main">Skip to content</a>
<header class="masthead">
<img src="images/header_title_4.gif" width="400" height="34" alt="HTTrack Website Copier">
<img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier">
<div class="tagline">Open Source offline browser</div>
</header>

View File

@@ -28,7 +28,7 @@ function info(str) {
<table width="76%" border="0" align="center" cellspacing="0" cellpadding="0" class="tableWidth">
<tr>
<td><img src="images/header_title_4.gif" width="400" height="34" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
<td><img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
</tr>
</table>

View File

@@ -64,7 +64,7 @@ function info(str) {
<table width="76%" border="0" align="center" cellspacing="0" cellpadding="0" class="tableWidth">
<tr>
<td><img src="images/header_title_4.gif" width="400" height="34" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
<td><img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
</tr>
</table>
<table width="76%" border="0" align="center" cellspacing="0" cellpadding="3" class="tableWidth">

View File

@@ -49,7 +49,72 @@
</screenshot>
</screenshots>
<content_rating type="oars-1.1"/>
<!-- Newest first; tests/01_engine-version-macros.test enforces it. -->
<releases>
<release version="3.49.8" date="2026-06-07"/>
<release version="3.49.15" date="2026-07-30">
<description>
<ul>
<li>Save a page and everything it needs as one self-contained file</li>
<li>Read a site's sitemap, so pages nothing links to are still found</li>
<li>See what a new pass added, updated or removed</li>
<li>Updating a mirror no longer destroys a good local copy when a download fails or is interrupted</li>
<li>In the web interface, options ticked by default can be un-ticked again, the size limit applies to the whole site, and the finished mirror opens from its link</li>
</ul>
</description>
</release>
<release version="3.49.14" date="2026-07-24">
<description>
<ul>
<li>Save a mirror as a web archive, indexed and packaged for replay</li>
<li>Choose what the page footer says</li>
<li>A mirror saved to a folder with accented characters in its name lands in the right place</li>
</ul>
</description>
</release>
<release version="3.49.13" date="2026-07-18">
<description>
<ul>
<li>Mirror through a SOCKS5 proxy</li>
<li>Files of 2 GB and over are handled correctly on Windows and on 32-bit systems</li>
<li>The web interface offers options that used to be command-line only, among them a cookies file and a pause between downloads</li>
</ul>
</description>
</release>
<release version="3.49.12" date="2026-07-10">
<description>
<p>Links carrying accented characters are followed again, a page wrongly labelled as compressed is no longer lost, and a new option explains why a given address is kept or skipped.</p>
</description>
</release>
<release version="3.49.11" date="2026-07-05">
<description>
<ul>
<li>Audio and video sources are mirrored along with the page</li>
<li>A site's robots.txt is followed more closely, including its Allow rules and wildcards</li>
<li>A time limit now stops a slow download instead of waiting for it to end</li>
</ul>
</description>
</release>
<release version="3.49.10" date="2026-06-28">
<description>
<ul>
<li>Start from a cookies file, and space downloads out with a random pause</li>
<li>Ignore chosen query parameters when naming saved files</li>
<li>Resuming a partial download no longer duplicates bytes</li>
</ul>
</description>
</release>
<release version="3.49.9" date="2026-06-21">
<description>
<p>Saved files get the right type when the server and the address disagree about it.</p>
</description>
</release>
<release version="3.49.8" date="2026-06-20">
<description>
<ul>
<li>Secure downloads can go through a plain web proxy</li>
<li>Every size of a responsive image is fetched</li>
</ul>
</description>
</release>
</releases>
</component>

View File

@@ -27,7 +27,7 @@ function info(str) {
<table width="76%" border="0" align="center" cellspacing="0" cellpadding="0" class="tableWidth">
<tr>
<td><img src="images/header_title_4.gif" width="400" height="34" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
<td><img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
</tr>
</table>

View File

@@ -27,7 +27,7 @@ function info(str) {
<table width="76%" border="0" align="center" cellspacing="0" cellpadding="0" class="tableWidth">
<tr>
<td><img src="images/header_title_4.gif" width="400" height="34" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
<td><img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
</tr>
</table>

View File

@@ -27,7 +27,7 @@ function info(str) {
<table width="76%" border="0" align="center" cellspacing="0" cellpadding="0" class="tableWidth">
<tr>
<td><img src="images/header_title_4.gif" width="400" height="34" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
<td><img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
</tr>
</table>
<table width="76%" border="0" align="center" cellspacing="0" cellpadding="3" class="tableWidth">

View File

@@ -27,7 +27,7 @@ function info(str) {
<table width="76%" border="0" align="center" cellspacing="0" cellpadding="0" class="tableWidth">
<tr>
<td><img src="images/header_title_4.gif" width="400" height="34" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
<td><img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
</tr>
</table>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="501" height="456" viewBox="0 0 501 456">
<title>HTTrack rings</title>
<!-- Transparent, unlike the GIF this replaces: the panel's own #ccd shows through. -->
<g fill="#b9b9d0" fill-rule="evenodd">
<path d="M614.59 346.3A323.36 197.55 21.16 1 0 11.46 112.89A323.36 197.55 21.16 1 0 614.59 346.3 ZM590.57 315.66A262.58 160.99 22.18 1 0 104.26 117.43A262.58 160.99 22.18 1 0 590.57 315.66 Z"/>
<path d="M578.08 292.16A185.6 113.57 21.23 1 0 232.08 157.73A185.6 113.57 21.23 1 0 578.08 292.16 ZM564.4 274.56A150.77 92.55 22.26 1 0 285.34 160.32A150.77 92.55 22.26 1 0 564.4 274.56 Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 641 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@@ -34,7 +34,7 @@ function info(str) {
<table width="76%" border="0" align="center" cellspacing="0" cellpadding="0" class="tableWidth">
<tr>
<td><img src="images/header_title_4.gif" width="400" height="34" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
<td><img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
</tr>
</table>
<table width="76%" border="0" align="center" cellspacing="0" cellpadding="3" class="tableWidth">

View File

@@ -31,7 +31,7 @@ function info(str) {
<table width="76%" border="0" align="center" cellspacing="0" cellpadding="0" class="tableWidth">
<tr>
<td><img src="images/header_title_4.gif" width="400" height="34" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
<td><img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
</tr>
</table>

View File

@@ -31,7 +31,7 @@ function info(str) {
<table width="76%" border="0" align="center" cellspacing="0" cellpadding="0" class="tableWidth">
<tr>
<td><img src="images/header_title_4.gif" width="400" height="34" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
<td><img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
</tr>
</table>

View File

@@ -31,7 +31,7 @@ function info(str) {
<table width="76%" border="0" align="center" cellspacing="0" cellpadding="0" class="tableWidth">
<tr>
<td><img src="images/header_title_4.gif" width="400" height="34" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
<td><img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
</tr>
</table>

View File

@@ -31,7 +31,7 @@ function info(str) {
<table width="76%" border="0" align="center" cellspacing="0" cellpadding="0" class="tableWidth">
<tr>
<td><img src="images/header_title_4.gif" width="400" height="34" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
<td><img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
</tr>
</table>

View File

@@ -41,7 +41,7 @@ function str_replace(str,from,to) {
<table width="76%" border="0" align="center" cellspacing="0" cellpadding="0" class="tableWidth">
<tr>
<td><img src="images/header_title_4.gif" width="400" height="34" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
<td><img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
</tr>
</table>

View File

@@ -31,7 +31,7 @@ function info(str) {
<table width="76%" border="0" align="center" cellspacing="0" cellpadding="0" class="tableWidth">
<tr>
<td><img src="images/header_title_4.gif" width="400" height="34" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
<td><img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
</tr>
</table>

View File

@@ -31,7 +31,7 @@ function info(str) {
<table width="76%" border="0" align="center" cellspacing="0" cellpadding="0" class="tableWidth">
<tr>
<td><img src="images/header_title_4.gif" width="400" height="34" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
<td><img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
</tr>
</table>

View File

@@ -31,7 +31,7 @@ function info(str) {
<table width="76%" border="0" align="center" cellspacing="0" cellpadding="0" class="tableWidth">
<tr>
<td><img src="images/header_title_4.gif" width="400" height="34" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
<td><img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
</tr>
</table>

View File

@@ -31,7 +31,7 @@ function info(str) {
<table width="76%" border="0" align="center" cellspacing="0" cellpadding="0" class="tableWidth">
<tr>
<td><img src="images/header_title_4.gif" width="400" height="34" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
<td><img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
</tr>
</table>

View File

@@ -31,7 +31,7 @@ function info(str) {
<table width="76%" border="0" align="center" cellspacing="0" cellpadding="0" class="tableWidth">
<tr>
<td><img src="images/header_title_4.gif" width="400" height="34" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
<td><img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
</tr>
</table>

View File

@@ -31,7 +31,7 @@ function info(str) {
<table width="76%" border="0" align="center" cellspacing="0" cellpadding="0" class="tableWidth">
<tr>
<td><img src="images/header_title_4.gif" width="400" height="34" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
<td><img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
</tr>
</table>

View File

@@ -31,7 +31,7 @@ function info(str) {
<table width="76%" border="0" align="center" cellspacing="0" cellpadding="0" class="tableWidth">
<tr>
<td><img src="images/header_title_4.gif" width="400" height="34" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
<td><img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
</tr>
</table>

View File

@@ -64,7 +64,7 @@ function no_refresh() {
<table width="76%" border="0" align="center" cellspacing="0" cellpadding="0" class="tableWidth">
<tr>
<td><img src="images/header_title_4.gif" width="400" height="34" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
<td><img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
</tr>
</table>
<table width="76%" border="0" align="center" cellspacing="0" cellpadding="3" class="tableWidth">

View File

@@ -43,7 +43,7 @@ function info(str) {
<table width="76%" border="0" align="center" cellspacing="0" cellpadding="0" class="tableWidth">
<tr>
<td><img src="images/header_title_4.gif" width="400" height="34" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
<td><img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
</tr>
</table>
<table width="76%" border="0" align="center" cellspacing="0" cellpadding="3" class="tableWidth">

View File

@@ -52,7 +52,7 @@ function do_check_child() {
<table width="76%" border="0" align="center" cellspacing="0" cellpadding="0" class="tableWidth">
<tr>
<td><img src="images/header_title_4.gif" width="400" height="34" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
<td><img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
</tr>
</table>
<table width="76%" border="0" align="center" cellspacing="0" cellpadding="3" class="tableWidth">

View File

@@ -34,7 +34,7 @@ function info(str) {
<table width="76%" border="0" align="center" cellspacing="0" cellpadding="0" class="tableWidth">
<tr>
<td><img src="images/header_title_4.gif" width="400" height="34" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
<td><img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
</tr>
</table>
<table width="76%" border="0" align="center" cellspacing="0" cellpadding="3" class="tableWidth">

View File

@@ -36,7 +36,7 @@ a:active { text-decoration: underline; }
border-bottom: 6px solid #000;
padding: 10px; padding-top: 20px;
line-height: 1.65em;
background-image: url(images/bg_rings.gif);
background-image: url(images/bg_rings.svg);
background-repeat: no-repeat;
background-position: top right;
}

View File

@@ -60,6 +60,13 @@ proxytrack_SOURCES = proxy/main.c \
whttrackrundir = $(bindir)
whttrackrun_SCRIPTS = webhttrack
# $(datadir) only expands at make time, so substitute here rather than through
# AC_CONFIG_FILES (#887).
webhttrack: webhttrack.in Makefile
$(AM_V_GEN)$(SED) -e 's|@datadir[@]|$(datadir)|g' $(srcdir)/webhttrack.in >$@.tmp \
&& chmod +x $@.tmp && mv -f $@.tmp $@
CLEANFILES = webhttrack
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 \
@@ -88,7 +95,7 @@ libhttrack_la_LIBADD = $(THREADS_LIBS) $(ZLIB_LIBS) $(BROTLI_LIBS) $(ZSTD_LIBS)
libhttrack_la_CFLAGS = $(AM_CFLAGS) -DLIBHTTRACK_EXPORTS -DZLIB_CONST
libhttrack_la_LDFLAGS = $(AM_LDFLAGS) -version-info $(VERSION_INFO)
EXTRA_DIST = httrack.h htsstats.h webhttrack \
EXTRA_DIST = httrack.h htsstats.h webhttrack.in \
version.rc \
libhttrack.rc \
httrack.rc \

View File

@@ -62,7 +62,30 @@ Please visit our Website: http://www.httrack.com
#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"
/* Slot operations */
static hts_boolean back_tmpname(char *dest, size_t size, const char *save,
const char *ext);
hts_boolean back_spoolname(httrackp *opt, const char *save, char *dest,
size_t size) {
/* -p0 keeps no save name to derive from, so it counts instead. No separator:
path_html_utf8 brings its own, and is "" with no -O, where an added one
would make this absolute and spool into the filesystem root. */
if (opt->getmode == 0) {
if (!slprintfbuff(dest, size, "%s" HTS_TMPDIR "/tmpfile%d.tmp",
StringBuff(opt->path_html_utf8),
opt->state.tmpnameid++)) {
dest[0] = '\0';
return HTS_FALSE;
}
return HTS_TRUE;
}
return back_tmpname(dest, size, save, "tmp");
}
static int slot_can_be_cached_on_disk(const lien_back * back);
static int slot_can_be_cleaned(const lien_back * back);
static int slot_can_be_finalized(httrackp * opt, const lien_back * back);
@@ -217,6 +240,7 @@ void back_delete_all(httrackp * opt, cache_back * cache, struct_back * sback) {
if (filename != NULL) {
(void) UNLINK(filename);
back_tmpdir_drop(filename);
}
#else
/* clear entry content (but not yet the entry) */
@@ -311,6 +335,7 @@ static int back_index_ready(httrackp * opt, struct_back * sback, const char *adr
adr, fil, sav);
}
(void) UNLINK(fileback);
back_tmpdir_drop(fileback);
#else
itemback = (lien_back *) ptr;
#endif
@@ -483,22 +508,11 @@ int back_cleanup_background(httrackp * opt, cache_back * cache,
#ifndef HTS_NO_BACK_ON_DISK
/* temporarily serialize the entry on disk */
{
/* +16: room for the ".tmp" the url_sav form appends to a full-length
save name, so one buffer holds both shapes */
char BIGSTK tmpname[HTS_URLMAXSIZE * 2 + 16];
/* +32: room for the directory and extension back_spoolname() inserts */
char BIGSTK tmpname[HTS_URLMAXSIZE * 2 + 32];
char *filename;
hts_boolean named;
/* the -p0 name is not derived from url_sav, so it needs a buffer of
its own size rather than the save name's */
if (opt->getmode != 0) {
named =
slprintfbuff(tmpname, sizeof(tmpname), "%s.tmp", back[i].url_sav);
} else {
named = slprintfbuff(tmpname, sizeof(tmpname), "%stmpfile%d.tmp",
StringBuff(opt->path_html_utf8),
opt->state.tmpnameid++);
}
const hts_boolean named =
back_spoolname(opt, back[i].url_sav, tmpname, sizeof(tmpname));
filename = named ? strdupt(tmpname) : NULL;
if (filename != NULL) {
@@ -656,10 +670,6 @@ int back_nsoc_overall(const struct_back * sback) {
return n;
}
/* 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"
/* Build save's temporary as <dir>/<HTS_TMPDIR>/<name>.<ext>. Appending the
extension to save instead put it in the mirror namespace, so a site serving
<path>.bak had its copy taken as the backup and then unlinked (#774).
@@ -717,7 +727,7 @@ static int create_back_tmpfile(httrackp *opt, lien_back *const back,
/* same directory as the named case, so back_tmpdir_drop() only removes one
the engine made (#842) */
/* truncation here would collide distinct tmpnameid's onto one name */
if (!sprintfbuff(back->tmpfile_buffer, "%s/" HTS_TMPDIR "/tmp%d.%s",
if (!sprintfbuff(back->tmpfile_buffer, "%s" HTS_TMPDIR "/tmp%d.%s",
StringBuff(opt->path_html_utf8), opt->state.tmpnameid++,
ext)) {
hts_log_print(opt, LOG_WARNING, "temporary filename too long in %s",
@@ -819,6 +829,14 @@ static hts_boolean back_chunked_unterminated(const lien_back *const back) {
return back->is_chunk && back->chunk_blocksize != -1 ? HTS_TRUE : HTS_FALSE;
}
/* Past the terminating chunk, the line still owed is the optional trailer
section (RFC 9112 7.1.2), read and discarded like a header block. */
static hts_boolean back_in_chunk_trailers(const lien_back *const back) {
return back->status == STATUS_CHUNK_CR && back->chunk_blocksize == -1
? HTS_TRUE
: HTS_FALSE;
}
// objet (lien) téléchargé ou transféré depuis le cache
//
// fermer les paramètres de transfert,
@@ -3475,11 +3493,16 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
// réception de données depuis socket ou fichier
if (back[i].status) {
if (back[i].status == STATUS_WAIT_HEADERS) // recevoir par bloc de lignes
retour_fread = http_xfread1(&(back[i].r), 0);
if (back[i].status == STATUS_WAIT_HEADERS)
retour_fread = http_xfread1(&(back[i].r), HTS_XFREAD_LINE_BLOCK);
else if (back[i].status == STATUS_CHUNK_WAIT || back[i].status == STATUS_CHUNK_CR) { // recevoir longueur chunk en hexa caractère par caractère
// backuper pour lire dans le buffer chunk
htsblk r;
/* Block mode bounds the trailer section, which declares no length
of its own, by HTS_LINE_BLOCK_SIZE. */
const int chunk_read_mode = back_in_chunk_trailers(&back[i])
? HTS_XFREAD_LINE_BLOCK
: HTS_XFREAD_LINE;
memcpy(&r, &(back[i].r), sizeof(htsblk));
back[i].r.is_write = 0; // mémoire
@@ -3489,8 +3512,8 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
back[i].r.out = NULL;
back[i].r.is_file = 0;
//
// ligne par ligne
retour_fread = http_xfread1(&(back[i].r), -1);
// one line, or the whole trailer block
retour_fread = http_xfread1(&(back[i].r), chunk_read_mode);
// modifier et restaurer
back[i].chunk_adr = back[i].r.adr; // adresse
back[i].chunk_size = back[i].r.size; // taille taille chunk
@@ -3628,12 +3651,22 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
}
// Traitement des en têtes chunks ou en têtes
if (back[i].status == STATUS_CHUNK_WAIT || back[i].status == STATUS_CHUNK_CR) { // réception taille chunk en hexa ( après les en têtes, peut ne pas
if (back[i].chunk_size > 0
&& back[i].chunk_adr[back[i].chunk_size - 1] == 10) {
const hts_boolean in_trailers = back_in_chunk_trailers(&back[i]);
/* A chunk-size or chunk-CRLF line closes on its first LF, the
trailer section on the blank line ending it. Two LFs mean a
blank line only because the reader drops every CR. */
if (back[i].chunk_size > 0 &&
back[i].chunk_adr[back[i].chunk_size - 1] == 10 &&
(!in_trailers || back[i].chunk_size == 1 ||
back[i].chunk_adr[back[i].chunk_size - 2] == 10)) {
int chunk_size = -1;
char chunk_data[64];
if (back[i].chunk_size < 32) { // pas trop gros
if (in_trailers) {
chunk_size =
0; /* fields discarded, the blank line ends the body */
} else if (back[i].chunk_size < 32) { // not too big
char *chstrip = back[i].chunk_adr;
back[i].chunk_adr[back[i].chunk_size - 1] = '\0'; // octet nul
@@ -3817,12 +3850,6 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
}
}
}
/* Oops, trailers! */
if (back[i].r.keep_alive_trailers) {
/* fixme (not yet supported) */
}
}
}
@@ -3836,7 +3863,7 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
// NO! xxback[i].chunk_blocksize = 0;
}
} // taille buffer chunk > 1 && LF
} // chunk buffer holds a complete line
//
} else if (back[i].status == STATUS_WAIT_HEADERS) { // en têtes (avant le chunk si il est présent)
//

View File

@@ -155,6 +155,11 @@ hts_boolean back_finalize_backup(httrackp *opt, lien_back *const back,
/* Remove the reserved directory a temporary sat in, once the last slot sharing
it is done; a non-empty one just refuses. No-op outside that directory. */
void back_tmpdir_drop(const char *tmp);
/* Name the spool file of a frozen backlog slot, inside the reserved directory
no save name can spell. HTS_FALSE (dest emptied) if it would not fit.
Consumes an opt->state.tmpnameid under -p0. Note: utf-8. */
hts_boolean back_spoolname(httrackp *opt, const char *save, char *dest,
size_t size);
/* -#test=backswap: slots eligible for the on-disk ready table. */
int back_selftest_slot_swap(void);
void back_info(struct_back * sback, int i, int j, FILE * fp);

View File

@@ -211,6 +211,13 @@ void hts_backtrace_init(void) {
#endif
}
/* Why the report has no frames: a silent gap reads as a handler that died. */
static void print_no_trace(int fd, const char *msg, size_t len) {
if (write(fd, msg, len) != len) { /* no ssize_t: this is built on MSVC too */
/* sorry GCC */
}
}
void hts_print_backtrace(int fd) {
#ifdef USES_BACKTRACE
void *stack[256];
@@ -227,12 +234,15 @@ void hts_print_backtrace(int fd) {
symbolize_backtrace(stack, size, fd);
entered = 0;
}
} else {
/* An empty trace means the build carries no unwind tables. */
const char msg[] = "No stack trace available: unwinding failed\n";
print_no_trace(fd, msg, sizeof(msg) - 1);
}
#else
const char msg[] = "No stack trace available on this OS :(\n";
if (write(fd, msg, sizeof(msg) - 1) != sizeof(msg) - 1) {
/* sorry GCC */
}
print_no_trace(fd, msg, sizeof(msg) - 1);
#endif
}

View File

@@ -52,6 +52,13 @@ Please visit our Website: http://www.httrack.com
#include "htsmd5.h"
#include <ctype.h>
/* hts_self_path() */
#ifdef __APPLE__
#include <mach-o/dyld.h>
#endif
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#if USE_BEGINTHREAD
#ifdef _WIN32
#include <process.h>
@@ -71,6 +78,95 @@ Please visit our Website: http://www.httrack.com
/* Resolver */
extern int IPV6_resolver;
/* A data directory is one that carries the templates path_bin is read for. */
static int datadir_has_templates(const char *dir) {
char catbuff[CATBUFF_SIZE];
return dir != NULL && *dir != '\0' &&
fexist(fconcat(catbuff, sizeof(catbuff), dir,
"templates/index-header.html"));
}
const char *hts_self_path(char *dst, size_t dstsize) {
#if defined(_WIN32)
const DWORD n = GetModuleFileNameA(NULL, dst, (DWORD) dstsize);
/* Pre-Win8 returns nSize on truncation without terminating: a full buffer
is a failure, not a path. */
return (n > 0 && (size_t) n < dstsize) ? dst : NULL;
#elif defined(__APPLE__)
uint32_t n = (uint32_t) dstsize;
return _NSGetExecutablePath(dst, &n) == 0 ? dst : NULL;
#else
/* Linux; anywhere else this is simply absent and argv[0] has to do. */
const ssize_t n = readlink("/proc/self/exe", dst, dstsize - 1);
if (n <= 0 || (size_t) n >= dstsize - 1)
return NULL;
dst[n] = '\0';
return dst;
#endif
}
/* Directory part of path, trailing '/' kept, or NULL when it carries none: a
bare name came from a PATH lookup and locates nothing. */
static const char *dirname_of(char *dst, size_t dstsize, const char *path) {
char catbuff[CATBUFF_SIZE];
const char *slashed, *sep;
size_t len;
if (path == NULL)
return NULL;
slashed = fslash(catbuff, sizeof(catbuff), path);
if ((sep = strrchr(slashed, '/')) == NULL)
return NULL;
len = (size_t) (sep - slashed) + 1;
if (len >= dstsize)
return NULL;
memcpy(dst, slashed, len);
dst[len] = '\0';
return dst;
}
void hts_resolve_datadir(char *dst, size_t dstsize, const char *selfpath,
const char *builtin) {
/* An installed tree that was moved, then a flat one with templates/ beside
the binary. */
static const char *const layout[] = {"../share/httrack/", ""};
char exedir[HTS_URLMAXSIZE * 2];
const char *base = NULL;
const char *fallback;
if (!datadir_has_templates(builtin)) {
base = dirname_of(exedir, sizeof(exedir), selfpath);
}
if (base != NULL) {
size_t i;
for (i = 0; i < sizeof(layout) / sizeof(layout[0]); i++) {
char cand[HTS_URLMAXSIZE * 2];
/* snprintf, not the strlncatbuff idiom: appending to a non-empty buffer
aborts on overflow, and a long enough argv[0] reaches it. */
const int n = snprintf(cand, sizeof(cand), "%s%s", base, layout[i]);
if (n < 0 || (size_t) n >= sizeof(cand)) {
continue; /* truncated, so not the path we meant to probe */
}
if (datadir_has_templates(cand)) {
snprintf(dst, dstsize, "%s", cand);
return;
}
}
}
/* Windows has no compiled-in data directory, so there the executable's own
is all we have. */
fallback = (builtin != NULL && *builtin != '\0') ? builtin
: (base != NULL) ? base
: "";
snprintf(dst, dstsize, "%s", fallback);
}
#define htsmain_free() do { \
if (url != NULL) { \
free(url); \
@@ -174,21 +270,25 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
}
#endif
// Binary program path?
#ifndef HTS_HTTRACKDIR
// Data directory holding the HTML templates
{
char catbuff[CATBUFF_SIZE];
char *path = fslash(catbuff, sizeof(catbuff), argv[0]);
char *a;
char datadir[HTS_URLMAXSIZE * 2];
char selfbuff[HTS_URLMAXSIZE * 2];
const char *self = hts_self_path(selfbuff, sizeof(selfbuff));
if ((a = strrchr(path, '/'))) {
StringCopyN(opt->path_bin, argv[0], a - path);
}
}
#ifdef HTS_HTTRACKDIR
const char *const builtin = HTS_HTTRACKDIR;
#else
StringCopy(opt->path_bin, HTS_HTTRACKDIR);
const char *const builtin = "";
#endif
if (self == NULL && argc > 0) {
self = argv[0];
}
hts_resolve_datadir(datadir, sizeof(datadir), self, builtin);
StringCopy(opt->path_bin, datadir);
}
/* filter CR, LF, TAB.. */
{
int na;

View File

@@ -47,6 +47,16 @@ Please visit our Website: http://www.httrack.com
int cmdl_opt(char *s);
int check_path(String * s, char *defaultname);
/* Absolute path of the running executable, or NULL where the OS will not say
and argv[0] is the only clue left. Fills dst (dstsize bytes). */
const char *hts_self_path(char *dst, size_t dstsize);
/* Write the data directory holding the HTML templates into dst (dstsize bytes,
NUL-terminated, trailing '/' included): builtin when it is there, else a
layout under selfpath's directory. selfpath may be NULL, builtin empty. */
void hts_resolve_datadir(char *dst, size_t dstsize, const char *selfpath,
const char *builtin);
#endif
#endif

View File

@@ -1834,12 +1834,8 @@ int check_writeinput_t(T_SOC soc, int timeout) {
return 0;
}
// idem, sauf qu'ici on peut choisir la taille max de données à recevoir
// SI bufl==0 alors le buffer est censé être de 8kos, et on recoit par bloc de lignes
// en éliminant les cr (ex: header), arrêt si double-lf
// SI bufl==-1 alors le buffer est censé être de 8kos, et on recoit ligne par ligne
// en éliminant les cr (ex: header), arrêt si double-lf
// Note: les +1 dans les malloc sont dûs à l'octet nul rajouté en fin de fichier
// Read one block: bufl is a byte count, or one of the HTS_XFREAD_* line modes.
// Note: the +1 in the mallocs is the trailing NUL appended to the data.
LLint http_xfread1(htsblk * r, int bufl) {
int nl = -1;
@@ -1952,14 +1948,7 @@ LLint http_xfread1(htsblk * r, int bufl) {
} // stockage disque ou mémoire
} else if (bufl == -2) { // force reserve
if (r->adr == NULL) {
r->adr = (char *) malloct(8192);
r->size = 0;
return 0;
}
return -1;
} else { // réception d'un en-tête octet par octet
} else { // line modes: byte by byte, CR dropped
int count = 256;
int tot_nl = 0;
int lf_detected = 0;
@@ -1969,18 +1958,16 @@ LLint http_xfread1(htsblk * r, int bufl) {
nl = READ_INTERNAL_ERROR;
count--;
if (r->adr == NULL) {
r->adr = (char *) malloct(8192);
r->adr = (char *) malloct(HTS_LINE_BLOCK_SIZE);
r->size = 0;
}
if (r->adr != NULL) {
if (r->size < 8190) {
if (r->size < HTS_LINE_BLOCK_SIZE - 2) {
// lecture
nl = hts_read(r, r->adr + r->size, 1);
if (nl > 0) {
// exit if:
// lf detected AND already detected before
// or
// lf detected AND first character read
// exit on a blank line (LF seen twice, or LF as the first byte),
// or on the first LF in HTS_XFREAD_LINE mode
if (*(r->adr + r->size) == 10) {
if (lf_detected || (at_beginning) || (bufl < 0))
count = -1;
@@ -2045,7 +2032,7 @@ htsblk http_test(httrackp * opt, const char *adr, const char *fil, char *loc) {
// tant qu'on a des données, et qu'on ne recoit pas deux LF, et que le timeout n'arrie pas
do {
if (http_xfread1(&retour, 0) < 0)
if (http_xfread1(&retour, HTS_XFREAD_LINE_BLOCK) < 0)
e = 1;
else {
if (retour.adr != NULL) {

View File

@@ -248,6 +248,14 @@ void treathead(t_cookie * cookie, const char *adr, const char *fil, htsblk * ret
void treatfirstline(htsblk * retour, const char *rcvd);
// sous-fonctions
/* Buffer http_xfread1() fills in its line modes, and so the ceiling on any
blank-line-terminated block it reads: a header section or a chunk trailer
section. Overrunning it fails the transfer. */
#define HTS_LINE_BLOCK_SIZE 8192
/* http_xfread1() read modes: a positive bufl reads at most that many raw bytes,
anything else reads CR-stripped lines into the HTS_LINE_BLOCK_SIZE buffer. */
#define HTS_XFREAD_LINE_BLOCK 0 /* lines up to a blank one (header/trailer) */
#define HTS_XFREAD_LINE (-1) /* one line, stopping at the first LF */
LLint http_xfread1(htsblk * r, int bufl);
/* Cached resolver: fill out[0..count-1] with up to max addresses for iadr (in
resolver order), returning the count (0 = does not resolve, negative-cached).

View File

@@ -54,6 +54,7 @@ Please visit our Website: http://www.httrack.com
#include "htsdns_selftest.h"
#include "htscharset.h"
#include "htscmdline.h"
#include "htscoremain.h"
#include "htsencoding.h"
#include "htsftp.h"
#include "htsmd5.h"
@@ -79,6 +80,7 @@ Please visit our Website: http://www.httrack.com
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wchar.h>
#ifndef _WIN32
#include <sys/socket.h>
#include <unistd.h>
@@ -3314,6 +3316,104 @@ static int st_makeindex(httrackp *opt, int argc, char **argv) {
return 0;
}
static void datadir_expect(const char *selfpath, const char *builtin,
const char *expect) {
char got[HTS_URLMAXSIZE * 2];
hts_resolve_datadir(got, sizeof(got), selfpath, builtin);
if (strcmp(got, expect) != 0) {
fprintf(stderr,
"datadir: self=%s builtin=\"%s\" gave \"%s\", expected \"%s\"\n",
selfpath != NULL ? selfpath : "(null)", builtin, got, expect);
}
assertf(strcmp(got, expect) == 0);
}
// -#test=datadir <dir>: a relocated tree must find its own templates instead of
// silently falling back to the built-in ones (#894). argv[0] is writable.
static int st_datadir(httrackp *opt, int argc, char **argv) {
char path[HTS_URLMAXSIZE];
char self[HTS_URLMAXSIZE];
char expect[HTS_URLMAXSIZE * 2];
char installed[HTS_URLMAXSIZE];
char gone[HTS_URLMAXSIZE];
/* Each holds a templates/index-header.html, the file path_bin is read for.
nest/ keeps the flat case away from the installed share/httrack above. */
static const char *const dirs[] = {"share/httrack", "bin", "nest/flat"};
size_t i;
(void) opt;
assertf(argc >= 1);
/* argv[0] is a fallback: what the engine actually resolves from is this. */
assertf(hts_self_path(path, sizeof(path)) != NULL);
assertf(fexist(path));
/* Too small for any real path, so the truncation guard must refuse. */
assertf(hts_self_path(path, 2) == NULL);
for (i = 0; i < sizeof(dirs) / sizeof(dirs[0]); i++) {
FILE *fp;
snprintf(path, sizeof(path), "%s/%s/templates/", argv[0], dirs[i]);
assertf(structcheck(path) == 0);
snprintf(path, sizeof(path), "%s/%s/templates/index-header.html", argv[0],
dirs[i]);
fp = fopen(path, "wb");
assertf(fp != NULL);
fclose(fp);
}
snprintf(installed, sizeof(installed), "%s/share/httrack/", argv[0]);
snprintf(gone, sizeof(gone), "%s/gone/", argv[0]);
/* A moved install: bin/ carries templates too, so this pins the order. */
snprintf(self, sizeof(self), "%s/bin/httrack", argv[0]);
snprintf(expect, sizeof(expect), "%s/bin/../share/httrack/", argv[0]);
datadir_expect(self, gone, expect);
/* The compiled-in path still wins when it exists. */
datadir_expect(self, installed, installed);
/* Flat layout: templates/ sits beside the binary. */
snprintf(self, sizeof(self), "%s/nest/flat/httrack", argv[0]);
snprintf(expect, sizeof(expect), "%s/nest/flat/", argv[0]);
datadir_expect(self, gone, expect);
/* Nothing to derive from, or nothing found: the compiled-in path stands. */
datadir_expect("httrack", gone, gone);
datadir_expect(NULL, gone, gone);
snprintf(self, sizeof(self), "%s/nowhere/deep/httrack", argv[0]);
datadir_expect(self, gone, gone);
/* No compiled-in path, as on Windows: the executable's own directory. */
snprintf(self, sizeof(self), "%s/nowhere/deep/httrack", argv[0]);
snprintf(expect, sizeof(expect), "%s/nowhere/deep/", argv[0]);
datadir_expect(self, "", expect);
datadir_expect("httrack", "", "");
/* A directory part too long for the layout suffix to be appended must clip,
not abort: appending to a non-empty buffer is the *_safe_ abort path. */
{
/* Long enough that dirname + "../share/httrack/" overflows the candidate
buffer, short enough that the dirname itself still fits. */
const size_t dirlen = HTS_URLMAXSIZE * 2 - 8;
char huge[HTS_URLMAXSIZE * 3];
char got[HTS_URLMAXSIZE * 2];
size_t n;
huge[0] = '/';
for (n = 1; n < dirlen - 1; n++) {
huge[n] = 'a';
}
huge[dirlen - 1] = '/';
memcpy(huge + dirlen, "httrack", sizeof("httrack"));
hts_resolve_datadir(got, sizeof(got), huge, gone);
assertf(strcmp(got, gone) == 0);
}
printf("datadir self-test OK\n");
return 0;
}
// hts_buildtopindex() writes a system-charset name into a charset=utf-8 doc: on
// Windows the gifs land in a mangled twin dir (#217) and a listed name renders
// as mojibake (#216). Both must come out utf-8. argv[0] is writable.
@@ -6468,6 +6568,88 @@ static int st_refetchbackup(httrackp *opt, int argc, char **argv) {
return err;
}
// -#test=spoolname <dir>: a frozen backlog slot must spool inside ~hts-tmp, not
// beside the mirrored file where a site serving <path>.tmp collides (#859).
static int st_spoolname(httrackp *opt, int argc, char **argv) {
char BIGSTK got[HTS_URLMAXSIZE * 2 + 32];
char BIGSTK want[HTS_URLMAXSIZE * 2 + 32];
char BIGSTK save[HTS_URLMAXSIZE * 2];
int err = 0;
if (argc < 1) {
fprintf(stderr, "spoolname: needs a writable base dir\n");
return 1;
}
/* named: the spool lands in the save name's own ~hts-tmp, which no URL can
spell since url_savename() maps '~' to '_' */
snprintf(save, sizeof(save), "%s/sub/page.html", argv[0]);
snprintf(want, sizeof(want), "%s/sub/~hts-tmp/page.html.tmp", argv[0]);
opt->getmode = 1;
if (!back_spoolname(opt, save, got, sizeof(got))) {
fprintf(stderr, "spoolname: naming failed for %s\n", save);
err++;
} else if (strcmp(got, want) != 0) {
fprintf(stderr, "spoolname: got %s, want %s\n", got, want);
err++;
}
/* pin the pre-#859 name as forbidden too: a site serving sub/page.html.tmp
was mirrored straight onto it */
snprintf(want, sizeof(want), "%s.tmp", save);
if (strcmp(got, want) == 0) {
fprintf(stderr, "spoolname: still spooling into the mirror namespace\n");
err++;
}
/* -p0 keeps no save name, so the spool counts inside path_html's ~hts-tmp */
{
char BIGSTK base[HTS_URLMAXSIZE * 2];
snprintf(base, sizeof(base), "%s/", argv[0]);
StringCopy(opt->path_html_utf8, base);
opt->getmode = 0;
opt->state.tmpnameid = 7;
snprintf(want, sizeof(want), "%s/~hts-tmp/tmpfile7.tmp", argv[0]);
if (!back_spoolname(opt, "", got, sizeof(got))) {
fprintf(stderr, "spoolname: naming failed under -p0\n");
err++;
} else if (strcmp(got, want) != 0) {
fprintf(stderr, "spoolname: -p0 got %s, want %s\n", got, want);
err++;
}
if (opt->state.tmpnameid != 8) {
fprintf(stderr, "spoolname: -p0 did not consume a tmpnameid\n");
err++;
}
}
/* with no -O, path_html_utf8 is empty and the spool must stay relative to
the working directory; a separator of our own would put it in / */
StringCopy(opt->path_html_utf8, "");
opt->getmode = 0;
opt->state.tmpnameid = 0;
if (!back_spoolname(opt, "", got, sizeof(got))) {
fprintf(stderr, "spoolname: naming failed with no output directory\n");
err++;
} else if (strcmp(got, "~hts-tmp/tmpfile0.tmp") != 0) {
fprintf(stderr, "spoolname: no -O gave %s, want ~hts-tmp/tmpfile0.tmp\n",
got);
err++;
}
/* too long must empty dest, not hand back a truncated name landing
somewhere real */
opt->getmode = 1;
if (back_spoolname(opt, save, got, 8) || got[0] != '\0') {
fprintf(stderr, "spoolname: an overlong name was not rejected\n");
err++;
}
printf("spoolname: %s\n", err ? "FAIL" : "OK");
return err;
}
// -#test=direnum <dir>: enumerate a long+non-ASCII directory via the
// opendir/readdir wrappers; children must round-trip as UTF-8 (#133,#630).
static int st_direnum(httrackp *opt, int argc, char **argv) {
@@ -7450,6 +7632,149 @@ static int st_rtrim(httrackp *opt, int argc, char **argv) {
return err;
}
/* Format LEN bytes of EXPECTED into S as two arguments, and check what came
back. HEAD and TAIL are scratch buffers of at least LEN+1 bytes. */
static int strsprintf_case(String *s, const char *expected, size_t len,
char *head, char *tail) {
const size_t half = len / 2;
memcpy(head, expected, half);
head[half] = '\0';
memcpy(tail, expected + half, len - half);
tail[len - half] = '\0';
StringSprintf(*s, "%s%s", head, tail);
return StringLength(*s) == len &&
memcmp(StringBuff(*s), expected, len) == 0 &&
StringBuff(*s)[len] == '\0';
}
/* StringSprintf_ stores the terminator at buffer[ret], so its `ret < capacity`
guard is off by one byte at the exact fill: an output whose length equals the
capacity writes past the allocation (#836). The lengths that reach it are the
capacities themselves, floored at 256 and doubling from there. */
static int st_strsprintf(httrackp *opt, int argc, char **argv) {
static const size_t caps[] = {256, 512, 1024, 2048};
enum { maxLen = 2100 };
char *expected = malloct(maxLen + 1);
char *head = malloct(maxLen + 1);
char *tail = malloct(maxLen + 1);
String reused = STRING_EMPTY;
size_t i, len;
int err = 0;
(void) opt;
(void) argc;
(void) argv;
if (expected == NULL || head == NULL || tail == NULL) {
printf("strsprintf self-test: FAIL (out of memory)\n");
return 1;
}
for (i = 0; i < maxLen; i++)
expected[i] = (char) ('a' + (i % 26));
expected[maxLen] = '\0';
/* one call on a String whose capacity is pinned to the boundary, so len ==
capacity is reached exactly once per boundary */
for (i = 0; !err && i < sizeof(caps) / sizeof(caps[0]); i++) {
for (len = caps[i] - 3; !err && len <= caps[i] + 3; len++) {
String s = STRING_EMPTY;
StringRoomTotal(s, caps[i]);
if (StringCapacity(s) != caps[i]) {
printf(" FAIL: capacity %u pinned to %u\n", (unsigned) caps[i],
(unsigned) StringCapacity(s));
err = 1;
} else if (!strsprintf_case(&s, expected, len, head, tail)) {
printf(" FAIL: length %u at capacity %u\n", (unsigned) len,
(unsigned) caps[i]);
err = 1;
}
StringFree(s);
}
}
/* the same String reused: its capacity grows under it between calls, and a
shorter output must not leave the previous one behind */
for (len = 0; !err && len <= maxLen; len++) {
if (!strsprintf_case(&reused, expected, len, head, tail)) {
printf(" FAIL: growing length %u\n", (unsigned) len);
err = 1;
}
}
for (len = maxLen + 1; !err && len-- > 0;) {
if (!strsprintf_case(&reused, expected, len, head, tail)) {
printf(" FAIL: shrinking length %u\n", (unsigned) len);
err = 1;
}
}
StringFree(reused);
/* The give-up path: an argument libc cannot convert fails at every capacity,
so the retry loop climbs to STRING_SPRINTF_MAX and then empties the
String. Probe libc first -- a platform that formats an unpaired surrogate
without faulting never reaches the path. */
{
static const wchar_t bad[] = {(wchar_t) 0xd800, 0};
char probe[32];
if (snprintf(probe, sizeof(probe), "%ls", bad) < 0) {
String s = STRING_EMPTY;
StringCopy(s, "leftover");
StringSprintf(s, "%ls", bad);
if (StringNotEmpty(s) || StringBuff(s) == NULL ||
StringBuff(s)[0] != '\0') {
printf(" FAIL: a failed conversion left %u bytes behind\n",
(unsigned) StringLength(s));
err = 1;
}
StringFree(s);
} else { /* stderr: test 150 pins stdout to the one-line verdict */
fprintf(stderr, " (skipped: this libc formats an unconvertible wide "
"string)\n");
}
}
/* StringSprintf empties the String when it gives up, and the WebDAV
enumeration pops the trailing '/' right after: on an empty String an
unguarded pop would wrap the unsigned length and write off the end. */
{
String never = STRING_EMPTY;
String cleared = STRING_EMPTY;
StringPopRight(never); /* never written to: buffer_ is still NULL */
if (StringLength(never) != 0 || StringBuff(never) != NULL) {
printf(" FAIL: pop on an unallocated String\n");
err = 1;
}
StringClear(cleared);
StringPopRight(cleared);
if (StringLength(cleared) != 0 || StringBuff(cleared)[0] != '\0') {
printf(" FAIL: pop on an emptied String\n");
err = 1;
}
/* control: the guard must not swallow a pop that has a byte to drop */
StringSprintf(cleared, "ab");
StringPopRight(cleared);
if (StringLength(cleared) != 1 || strcmp(StringBuff(cleared), "a") != 0) {
printf(" FAIL: pop on a non-empty String\n");
err = 1;
}
StringFree(never);
StringFree(cleared);
}
freet(expected);
freet(head);
freet(tail);
printf("strsprintf self-test: %s\n", err ? "FAIL" : "OK");
return err;
}
/* ------------------------------------------------------------ */
/* Registry: name -> handler, with a usage hint and a one-line description. */
/* ------------------------------------------------------------ */
@@ -7504,6 +7829,8 @@ static const struct selftest_entry {
{"hashtable", "<count|file>", "coucal hashtable stress test", st_hashtable},
{"strsafe", "[overflow|overflow-buff|overflow-src [str]]",
"bounded string-op self-test", st_strsafe},
{"strsprintf", "", "StringSprintf grows to fit at every capacity boundary",
st_strsprintf},
{"copyopt", "", "copy_htsopt option-copy self-test", st_copyopt},
{"lastchar", "",
"last-char helpers never index before the buffer (#770, #781, #821)",
@@ -7585,6 +7912,9 @@ static const struct selftest_entry {
{"topindex", "[dir]",
"hts_buildtopindex charset handling of a non-ASCII project dir",
st_topindex},
{"datadir", "<dir>",
"data directory resolution: compiled-in path, then the executable's tree",
st_datadir},
{"inplace-escape", "", "inplace_escape_* vs escape_* equivalence self-test",
st_inplace_escape},
{"escape-room", "", "HT_ADD_HTMLESCAPED* reservation-factor self-test",
@@ -7631,6 +7961,8 @@ static const struct selftest_entry {
{"refetchbackup", "<dir>",
"the re-fetch backup always leaves a copy, and stays out of the mirror",
st_refetchbackup},
{"spoolname", "<dir>",
"a frozen backlog slot spools outside the mirror namespace", st_spoolname},
{"direnum", "<dir>",
"enumerate a long+non-ASCII directory through opendir/readdir",
st_direnum},

View File

@@ -36,6 +36,9 @@ Please visit our Website: http://www.httrack.com
#define HTS_STRINGS_DEFSTATIC
/* System definitions. */
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* GCC extension */
@@ -91,10 +94,6 @@ struct String {
#define STRING_FREE(BUFF) free(BUFF)
#endif
#ifndef STRING_ASSERT
#include <assert.h>
#define STRING_ASSERT(EXP) assert(EXP)
#endif
/** Initializer for an empty String (NULL buffer). Use to declare or reset. **/
#define STRING_EMPTY {(char *) NULL, 0, 0}
@@ -127,27 +126,44 @@ struct String {
/** Byte POS positions from the end (read/write). POS==1 is the last byte. **/
#define StringRightRW(BLK, POS) (StringBuffRW(BLK)[StringLength(BLK) - POS])
/** Drop the last byte and re-terminate. Undefined if the String is empty
(no length check; would underflow). **/
/** Drop the last byte and re-terminate. No-op on an empty String: the length
is unsigned, so an unguarded pop would wrap it and write off the end. **/
#define StringPopRight(BLK) \
do { \
StringBuffRW(BLK)[--StringLength(BLK)] = '\0'; \
if (StringLength(BLK) > 0) { \
StringBuffRW(BLK)[--StringLength(BLK)] = '\0'; \
} \
} while (0)
/** Terminate on allocation failure. The String API returns void throughout, so
there is no channel to report it on, and continuing would hand the caller a
NULL buffer to write into. **/
HTS_STATIC void StringOom_(size_t size) {
fprintf(stderr, "String: out of memory allocating %lu bytes\n",
(unsigned long) size);
fflush(stderr); /* abort() flushes nothing; Windows buffers a piped stderr */
abort();
}
/** What to do when an allocation of SIZE bytes fails; overridable. **/
#ifndef STRING_OOM
#define STRING_OOM(SIZE) StringOom_(SIZE)
#endif
/** Grow so capacity_ >= CAPACITY (total bytes, including the NUL). May realloc
(invalidating prior buffer pointers); aborts via STRING_ASSERT on OOM.
Never shrinks. **/
(invalidating prior buffer pointers); aborts on OOM. Never shrinks. **/
#define StringRoomTotal(BLK, CAPACITY) \
do { \
const size_t capacity_ = (size_t) (CAPACITY); \
while ((BLK).capacity_ < capacity_) { \
if ((BLK).capacity_ < 16) { \
(BLK).capacity_ = 16; \
} else { \
(BLK).capacity_ *= 2; \
const size_t newcap_ = (BLK).capacity_ < 16 ? 16 : (BLK).capacity_ * 2; \
char *const buff_ = STRING_REALLOC((BLK).buffer_, newcap_); \
\
if (buff_ == NULL) { \
STRING_OOM(newcap_); \
} \
(BLK).buffer_ = STRING_REALLOC((BLK).buffer_, (BLK).capacity_); \
STRING_ASSERT((BLK).buffer_ != NULL); \
(BLK).buffer_ = buff_; \
(BLK).capacity_ = newcap_; \
} \
} while (0)
@@ -165,6 +181,46 @@ HTS_STATIC char *StringBuffN_(String *blk, int size) {
return StringBuffRW(*blk);
}
/** Ceiling on the pre-C99 doubling search below; BLK is emptied past it. **/
#define STRING_SPRINTF_MAX ((size_t) 16 * 1024 * 1024)
/** Replace BLK's contents with the formatted output, growing to fit, so no
fixed reserve has to bound an argument carrying remote input. An argument
may not point into BLK's own buffer, which is reallocated here.
Leaves BLK empty if the output cannot be produced (a conversion error, or
past STRING_SPRINTF_MAX): callers must not assume a non-empty result. **/
#define StringSprintf(BLK, ...) StringSprintf_(&(BLK), __VA_ARGS__)
HTS_STATIC HTS_PRINTF_FUN(2, 3) void StringSprintf_(String *blk,
const char *fmt, ...) {
size_t capacity = StringCapacity(*blk) > 256 ? StringCapacity(*blk) : 256;
for (;;) {
va_list args;
int ret;
StringRoomTotal(*blk, capacity);
va_start(args, fmt);
ret = vsnprintf(StringBuffRW(*blk), capacity, fmt, args);
va_end(args);
if (ret >= 0 && (size_t) ret < capacity) {
StringBuffRW(*blk)[ret] = '\0';
StringLength(*blk) = (size_t) ret;
return;
}
if (ret >= 0) {
capacity = (size_t) ret + 1; /* C99 said what it needs */
} else if (capacity < STRING_SPRINTF_MAX) {
capacity *= 2; /* pre-C99 msvcrt only says "too small" */
} else {
/* a conversion error returns -1 too, and no capacity ever fixes that */
StringBuffRW(*blk)[0] = '\0';
StringLength(*blk) = 0;
return;
}
}
}
/** Zero the fields (NULL buffer, no allocation). Use on an uninitialized
String only; does NOT free an existing buffer (use StringFree to reset
an owned one), so calling it on a live String leaks. **/

View File

@@ -37,9 +37,33 @@ Please visit our Website: http://www.httrack.com
#include "htsbase.h"
#include "htsnet.h"
#include "htslib.h"
#include "coucal.h"
#include "store.h"
#include "proxytrack.h"
/* HTS_LOG, the engine's own debug switch. */
static hts_boolean proxytrack_verbose = HTS_FALSE;
/* Without a handler coucal writes to stderr itself, prefixing the table
address; the per-enumeration statistics summary then lands on the operator's
console once per WebDAV request (#918). */
static void proxytrack_coucal_loghandler(coucal_opaque arg,
coucal_loglevel level,
const char *format, va_list args) {
const char *severity;
(void) arg;
if (level <= coucal_log_critical) {
severity = CRITICAL;
} else if (level <= coucal_log_warning) {
severity = WARNING;
} else {
/* not the DEBUG macro: it is NULL outside a debug build, voiding HTS_LOG */
severity = proxytrack_verbose ? "debug" : NULL;
}
proxytrack_vprint_log(severity, format, args);
}
#ifndef _WIN32
#include <signal.h>
static void sig_brpipe(int code) {
@@ -90,6 +114,17 @@ int main(int argc, char *argv[]) {
}
#endif
/* Before the first table is built. */
{
const char *const dbg_env = getenv("HTS_LOG");
int level = 0;
if (dbg_env != NULL && sscanf(dbg_env, "%d", &level) == 1 && level > 0) {
proxytrack_verbose = HTS_TRUE;
}
}
coucal_set_global_assert_handler(proxytrack_coucal_loghandler, NULL);
/* Args */
printf("ProxyTrack %s, build proxies upon HTTrack Website Copier Archives\n",
PROXYTRACK_VERSION);

View File

@@ -548,29 +548,37 @@ static void proxytrack_add_DAV_Item(String * item, String * buff,
name = "Default Document for the Folder";
}
StringRoom(*item, 1024);
sprintf(StringBuffRW(*item),
"<response xmlns=\"DAV:\">\r\n" "<href>/webdav%s%s</href>\r\n"
"<propstat>\r\n" "<prop>\r\n" "<displayname>%s</displayname>\r\n"
"<iscollection>%d</iscollection>\r\n"
"<haschildren>%d</haschildren>\r\n" "<isfolder>%d</isfolder>\r\n"
"<resourcetype>%s</resourcetype>\r\n"
"<creationdate>%d-%02d-%02dT%02d:%02d:%02dZ</creationdate>\r\n"
"<getlastmodified>%s</getlastmodified>\r\n"
"<supportedlock></supportedlock>\r\n" "<lockdiscovery/>\r\n"
"<getcontenttype>%s</getcontenttype>\r\n"
"<getcontentlength>%d</getcontentlength>\r\n"
"<isroot>%d</isroot>\r\n" "</prop>\r\n"
"<status>HTTP/1.1 200 OK</status>\r\n" "</propstat>\r\n"
"</response>\r\n",
/* */
(StringBuff(*buff)[0] == '/') ? "" : "/", StringBuff(*buff), name,
isDir ? 1 : 0, isDir ? 1 : 0, isDir ? 1 : 0,
isDir ? "<collection/>" : "", timetm->tm_year + 1900,
timetm->tm_mon + 1, timetm->tm_mday, timetm->tm_hour,
timetm->tm_min, timetm->tm_sec, tms,
isDir ? "httpd/unix-directory" : mime, (int) size, isRoot ? 1 : 0);
StringLength(*item) = (int) strlen(StringBuff(*item));
/* The path lands here twice and escapexml() expands '&' fivefold, so no
fixed reserve bounds it (#836). */
StringSprintf(
*item,
"<response xmlns=\"DAV:\">\r\n"
"<href>/webdav%s%s</href>\r\n"
"<propstat>\r\n"
"<prop>\r\n"
"<displayname>%s</displayname>\r\n"
"<iscollection>%d</iscollection>\r\n"
"<haschildren>%d</haschildren>\r\n"
"<isfolder>%d</isfolder>\r\n"
"<resourcetype>%s</resourcetype>\r\n"
"<creationdate>%d-%02d-%02dT%02d:%02d:%02dZ</creationdate>\r\n"
"<getlastmodified>%s</getlastmodified>\r\n"
"<supportedlock></supportedlock>\r\n"
"<lockdiscovery/>\r\n"
"<getcontenttype>%s</getcontenttype>\r\n"
"<getcontentlength>%d</getcontentlength>\r\n"
"<isroot>%d</isroot>\r\n"
"</prop>\r\n"
"<status>HTTP/1.1 200 OK</status>\r\n"
"</propstat>\r\n"
"</response>\r\n",
/* */
(StringBuff(*buff)[0] == '/') ? "" : "/", StringBuff(*buff), name,
isDir ? 1 : 0, isDir ? 1 : 0, isDir ? 1 : 0,
isDir ? "<collection/>" : "", timetm->tm_year + 1900,
timetm->tm_mon + 1, timetm->tm_mday, timetm->tm_hour, timetm->tm_min,
timetm->tm_sec, tms, isDir ? "httpd/unix-directory" : mime, (int) size,
isRoot ? 1 : 0);
}
}
@@ -721,11 +729,8 @@ static PT_Element proxytrack_process_DAV_Request(PT_Indexes indexes,
}
/* Form response */
StringRoom(response, 1024);
sprintf(StringBuffRW(response),
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n"
"<multistatus xmlns=\"DAV:\">\r\n");
StringLength(response) = (int) strlen(StringBuff(response));
StringSprintf(response, "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n"
"<multistatus xmlns=\"DAV:\">\r\n");
/* */
/* Root */
@@ -739,7 +744,6 @@ static PT_Element proxytrack_process_DAV_Request(PT_Indexes indexes,
if (depth > 0) {
time_t timestampRep = (time_t) - 1;
const char *prefix = StringBuff(url);
unsigned int prefixLen = (unsigned int) strlen(prefix);
char **list = PT_Enumerate(indexes, prefix, 0);
if (list != NULL) {
@@ -752,15 +756,13 @@ static PT_Element proxytrack_process_DAV_Request(PT_Indexes indexes,
int thisIsDir = (hts_lastchar(thisUrl) == '/') ? 1 : 0;
/* Item URL */
StringRoom(itemUrl,
thisUrlLen + prefixLen + sizeof("/webdav/") + 1);
StringClear(itemUrl);
sprintf(StringBuffRW(itemUrl), "/%s/%s", prefix, thisUrl);
if (!thisIsDir)
StringLength(itemUrl) = (int) strlen(StringBuff(itemUrl));
else
StringLength(itemUrl) = (int) strlen(StringBuff(itemUrl)) - 1;
StringBuffRW(itemUrl)[StringLength(itemUrl)] = '\0';
StringSprintf(itemUrl, "/%s/%s", prefix, thisUrl);
if (!StringNotEmpty(itemUrl)) { /* formatting gave up: unnameable */
continue;
}
if (thisIsDir) { /* drop the trailing '/' */
StringPopRight(itemUrl);
}
if (thisIsDir == isDir) {
size_t size = 0;
@@ -827,8 +829,6 @@ static PT_Element proxytrack_process_DAV_Request(PT_Indexes indexes,
strcpybuff(elt->msg, "Multi-Status");
StringFree(response);
fprintf(stderr, "RESPONSE:\n%s\n", elt->adr);
return elt;
}
return NULL;
@@ -1037,18 +1037,18 @@ static void proxytrack_process_HTTP(PT_Indexes indexes, T_SOC soc_c) {
const char *options = "GET, HEAD, OPTIONS, POST, PROPFIND, TRACE" ", MKCOL, DELETE, PUT"; /* Not supported */
msgCode = HTTP_OK;
StringRoom(headers, 8192);
sprintf(StringBuffRW(headers),
"HTTP/1.1 %d %s\r\n" "DAV: 1, 2\r\n" "MS-Author-Via: DAV\r\n"
"Cache-Control: private\r\n" "Allow: %s\r\n", msgCode,
GetHttpMessage(msgCode), options);
StringLength(headers) = (int) strlen(StringBuff(headers));
StringSprintf(headers,
"HTTP/1.1 %d %s\r\n"
"DAV: 1, 2\r\n"
"MS-Author-Via: DAV\r\n"
"Cache-Control: private\r\n"
"Allow: %s\r\n",
msgCode, GetHttpMessage(msgCode), options);
} else if (strcasecmp(command, "propfind") == 0) {
if (davDepth > 1) {
msgCode = 403;
msgError = "DAV Depth Limit Forbidden";
} else {
fprintf(stderr, "DEBUG: DAV-DATA=<%s>\n", StringBuff(davRequest));
listRequest = 2; /* propfind */
}
} else if (strcasecmp(command, "mkcol") == 0
@@ -1141,11 +1141,9 @@ static void proxytrack_process_HTTP(PT_Indexes indexes, T_SOC soc_c) {
proxytrack_process_DAV_Request(indexes, StringBuff(url),
davDepth)) != NULL) {
msgCode = element->statuscode;
StringRoom(davHeaders, 1024);
sprintf(StringBuffRW(davHeaders),
"DAV: 1, 2\r\n" "MS-Author-Via: DAV\r\n"
"Cache-Control: private\r\n");
StringLength(davHeaders) = (int) strlen(StringBuff(davHeaders));
StringSprintf(davHeaders, "DAV: 1, 2\r\n"
"MS-Author-Via: DAV\r\n"
"Cache-Control: private\r\n");
}
}
#endif
@@ -1164,45 +1162,46 @@ static void proxytrack_process_HTTP(PT_Indexes indexes, T_SOC soc_c) {
}
}
if (element != NULL) {
/* lifted out of the format: a directive inside a macro argument list
is undefined, and MSVC rejects it */
#ifndef NO_WEBDAV
const char *const davPart = StringBuff(davHeaders);
#else
const char *const davPart = "";
#endif
msgCode = element->statuscode;
StringRoom(headers, 8192);
sprintf(StringBuffRW(headers),
"HTTP/1.1 %d %s\r\n"
#ifndef NO_WEBDAV
"%s"
#endif
"Content-Type: %s%s%s%s\r\n"
"%s%s%s"
"%s%s%s"
"%s%s%s",
/* */
msgCode, element->msg,
#ifndef NO_WEBDAV
/* DAV */
StringBuff(davHeaders),
#endif
/* Content-type: foo; [ charset=bar ] */
hts_effective_mime(element->contenttype),
((element->charset[0]) ? "; charset=\"" : ""),
element->charset, ((element->charset[0]) ? "\"" : ""),
/* location */
((element->location != NULL && element->location[0])
? "Location: "
: ""),
((element->location != NULL && element->location[0])
? element->location
: ""),
((element->location != NULL && element->location[0]) ? "\r\n"
: ""),
/* last-modified */
((element->lastmodified[0]) ? "Last-Modified: " : ""),
((element->lastmodified[0]) ? element->lastmodified : ""),
((element->lastmodified[0]) ? "\r\n" : ""),
/* etag */
((element->etag[0]) ? "ETag: " : ""),
((element->etag[0]) ? element->etag : ""),
((element->etag[0]) ? "\r\n" : ""));
StringLength(headers) = (int) strlen(StringBuff(headers));
StringSprintf(
headers,
"HTTP/1.1 %d %s\r\n"
"%s"
"Content-Type: %s%s%s%s\r\n"
"%s%s%s"
"%s%s%s"
"%s%s%s",
/* */
msgCode, element->msg, davPart,
/* Content-type: foo; [ charset=bar ] */
hts_effective_mime(element->contenttype),
((element->charset[0]) ? "; charset=\"" : ""), element->charset,
((element->charset[0]) ? "\"" : ""),
/* location */
((element->location != NULL && element->location[0])
? "Location: "
: ""),
((element->location != NULL && element->location[0])
? element->location
: ""),
((element->location != NULL && element->location[0]) ? "\r\n"
: ""),
/* last-modified */
((element->lastmodified[0]) ? "Last-Modified: " : ""),
((element->lastmodified[0]) ? element->lastmodified : ""),
((element->lastmodified[0]) ? "\r\n" : ""),
/* etag */
((element->etag[0]) ? "ETag: " : ""),
((element->etag[0]) ? element->etag : ""),
((element->etag[0]) ? "\r\n" : ""));
} else {
/* No query string, no ending / : check the the <url>/ page */
if (StringLength(url) > 0
@@ -1212,29 +1211,32 @@ static void proxytrack_process_HTTP(PT_Indexes indexes, T_SOC soc_c) {
StringCat(urlRedirect, "/");
if (PT_LookupIndex(indexes, StringBuff(urlRedirect))) {
msgCode = 301; /* Moved Permanently */
StringRoom(headers, 8192);
sprintf(StringBuffRW(headers),
"HTTP/1.1 %d %s\r\n" "Content-Type: text/html\r\n"
"Location: %s\r\n",
/* */
msgCode, GetHttpMessage(msgCode), StringBuff(urlRedirect)
);
StringLength(headers) = (int) strlen(StringBuff(headers));
StringSprintf(headers,
"HTTP/1.1 %d %s\r\n"
"Content-Type: text/html\r\n"
"Location: %s\r\n",
/* */
msgCode, GetHttpMessage(msgCode),
StringBuff(urlRedirect));
/* */
StringRoom(output,
1024 + sizeof(PROXYTRACK_COMMENT_HEADER) +
sizeof(DISABLE_IE_FRIENDLY_HTTP_ERROR_MESSAGES));
sprintf(StringBuffRW(output),
"<html>" PROXYTRACK_COMMENT_HEADER
DISABLE_IE_FRIENDLY_HTTP_ERROR_MESSAGES "<head>"
"<title>ProxyTrack - Page has moved</title>" "</head>\r\n"
"<body>" "<h3>The correct location is:</h3><br />"
"<b><a href=\"%s\">%s</a></b><br />" "<br />" "<br />\r\n"
"<i>Generated by ProxyTrack " PROXYTRACK_VERSION
", (C) Xavier Roche and other contributors</i>" "\r\n"
"</body>" "</header>", StringBuff(urlRedirect),
StringBuff(urlRedirect));
StringLength(output) = (int) strlen(StringBuff(output));
StringSprintf(
output,
"<html"
">" PROXYTRACK_COMMENT_HEADER DISABLE_IE_FRIENDLY_HTTP_ERROR_MESSAGES
"<head>"
"<title>ProxyTrack - Page has moved</title>"
"</head>\r\n"
"<body>"
"<h3>The correct location is:</h3><br />"
"<b><a href=\"%s\">%s</a></b><br />"
"<br />"
"<br />\r\n"
"<i>Generated by ProxyTrack " PROXYTRACK_VERSION
", (C) Xavier Roche and other contributors</i>"
"\r\n"
"</body>"
"</header>",
StringBuff(urlRedirect), StringBuff(urlRedirect));
}
}
if (msgCode == 0) {
@@ -1255,25 +1257,29 @@ static void proxytrack_process_HTTP(PT_Indexes indexes, T_SOC soc_c) {
} else if (msgError == NULL) {
msgError = GetHttpMessage(msgCode);
}
StringRoom(headers, 256);
sprintf(StringBuffRW(headers),
"HTTP/1.1 %d %s\r\n" "Content-type: text/html\r\n", msgCode,
msgError);
StringLength(headers) = (int) strlen(StringBuff(headers));
StringRoom(output,
1024 + sizeof(PROXYTRACK_COMMENT_HEADER) +
sizeof(DISABLE_IE_FRIENDLY_HTTP_ERROR_MESSAGES));
sprintf(StringBuffRW(output),
"<html>" PROXYTRACK_COMMENT_HEADER
DISABLE_IE_FRIENDLY_HTTP_ERROR_MESSAGES "<head>"
"<title>ProxyTrack - HTTP Proxy Error %d</title>" "</head>\r\n"
"<body>"
"<h3>A proxy error has occurred while processing the request.</h3><br />"
"<b>Error HTTP %d: <i>%s</i></b><br />" "<br />" "<br />\r\n"
"<i>Generated by ProxyTrack " PROXYTRACK_VERSION
", (C) Xavier Roche and other contributors</i>" "\r\n" "</body>"
"</html>", msgCode, msgCode, msgError);
StringLength(output) = (int) strlen(StringBuff(output));
StringSprintf(headers,
"HTTP/1.1 %d %s\r\n"
"Content-type: text/html\r\n",
msgCode, msgError);
StringSprintf(
output,
"<html"
">" PROXYTRACK_COMMENT_HEADER DISABLE_IE_FRIENDLY_HTTP_ERROR_MESSAGES
"<head>"
"<title>ProxyTrack - HTTP Proxy Error %d</title>"
"</head>\r\n"
"<body>"
"<h3>A proxy error has occurred while processing the "
"request.</h3><br />"
"<b>Error HTTP %d: <i>%s</i></b><br />"
"<br />"
"<br />\r\n"
"<i>Generated by ProxyTrack " PROXYTRACK_VERSION
", (C) Xavier Roche and other contributors</i>"
"\r\n"
"</body>"
"</html>",
msgCode, msgCode, msgError);
}
{
char tmp[20 + 1]; /* 2^64 = 18446744073709551616 */

View File

@@ -62,22 +62,32 @@ int proxytrack_main(char *proxyAddr, int proxyPort, char *icpAddr, int icpPort,
/* Static definitions */
HTS_UNUSED static void proxytrack_print_log(const char *severity, const char *format, ...) {
/* Log one line; a NULL severity discards it. */
HTS_UNUSED static void proxytrack_vprint_log(const char *severity,
const char *format, va_list args) {
if (severity != NULL) {
const int error = errno;
FILE *const fp = stderr;
va_list args;
fprintf(fp, " * %s: ", severity);
va_start(args, format);
(void) vfprintf(fp, format, args);
va_end(args);
fputs("\n", fp);
fflush(fp);
errno = error;
}
}
HTS_UNUSED static void proxytrack_print_log(const char *severity,
const char *format, ...) {
if (severity != NULL) {
va_list args;
va_start(args, format);
proxytrack_vprint_log(severity, format, args);
va_end(args);
}
}
#define CRITICAL "critical"
#define WARNING "warning"
#define LOG "log"

View File

@@ -23,7 +23,9 @@ for d in "${pathdirs[@]}"; do
# drop empty PATH fields, matching the old echo|tr word-split
test -n "$d" && SRCHPATH+=("$d")
done
SRCHDISTPATH=("$BINWD/../share" "$BINWD/.." /usr/share /usr/local /usr /local /usr/local/share "${HOME}/usr" "${HOME}/usr/share" /opt/local/share /sw "${HOME}/usr/local" "${HOME}/usr/share")
# The substituted datadir goes after the relative entries so a moved tree wins, and
# before the guesses so --datadir works (#887).
SRCHDISTPATH=("$BINWD/../share" "$BINWD/.." "@datadir@" /usr/share /usr/local /usr /local /usr/local/share "${HOME}/usr" "${HOME}/usr/share" /opt/local/share /sw "${HOME}/usr/local" "${HOME}/usr/share")
###
# And now some famous cuisine

View File

@@ -3,7 +3,6 @@
# Regression guard for the unsigned-enum sentinel trap: copy_htsopt's
# `if (from->X > -1)` guard is always false for unsigned hts_boolean fields, so
# they silently stop being copied. Driven by the in-process 'httrack -#test=copyopt' test.
# Keep POSIX-portable (harness runs it via $(BASH), a plain /bin/sh on macOS).
set -eu

View File

@@ -1,9 +1,5 @@
#!/bin/bash
#
# Keep this POSIX-portable: the harness runs it via $(BASH), which is a plain
# POSIX /bin/sh on some platforms (e.g. macOS), so avoid bashisms despite the
# #!/bin/bash above.
# A -%F footer whose expansion overflows the on-page buffer must be dropped, not
# crash the crawl. Before the fix the unchecked hts_footer_format return left the
# buffer unterminated and the next strcatbuff aborted (SIGABRT).

View File

@@ -2,7 +2,7 @@
#
# --pause (#185): the inter-file pause target must stay in [min,max] and spread
# across it (a per-call rand() would collapse it toward min). Driven by the
# in-process 'httrack -#test=pause' test. POSIX-portable ($(BASH) is /bin/sh on macOS).
# in-process 'httrack -#test=pause' test.
set -eu

View File

@@ -1,15 +1,17 @@
#!/bin/bash
#
# version.rc repeats the version that htsglobal.h declares. Signing enforces that
# every binary in a release reports the same one, so a drift fails the signing
# request on release day rather than the build. Assert the two agree.
# htsglobal.h declares the version; version.rc, configure.ac and the metainfo
# repeat it. Miss one and a release ships a mismatched binary or a stale store entry.
set -euo pipefail
src="${top_srcdir:-..}/src"
top="${top_srcdir:-..}"
src="$top/src"
h="$src/htsglobal.h"
rc="$src/version.rc"
for f in "$h" "$rc"; do
ac="$top/configure.ac"
metainfo="$top/html/server/div/com.httrack.WebHTTrack.metainfo.xml"
for f in "$h" "$rc" "$ac" "$metainfo"; do
[ -f "$f" ] || {
echo "cannot find $f"
exit 1
@@ -31,23 +33,49 @@ rc_fileversion=$(sed -n 's/.*VALUE "FileVersion",[[:space:]]*"\([^"]*\)".*/\1/p'
rc_productversion=$(sed -n 's/.*VALUE "ProductVersion",[[:space:]]*"\([^"]*\)".*/\1/p' "$rc")
rc_productname=$(sed -n 's/.*VALUE "ProductName",[[:space:]]*"\([^"]*\)".*/\1/p' "$rc")
# And again in configure.ac and the AppStream metainfo.
ac_version=$(sed -n 's/^AC_INIT(\[[^]]*\],[[:space:]]*\[\([^]]*\)\].*/\1/p' "$ac")
# Comments dropped, one tag per line: a parked <release> or a version= on the
# <releases> wrapper would otherwise pose as the newest entry.
releases=$(sed 's/<!--.*-->//g' "$metainfo" | tr '<' '\n' |
sed -n 's/^release[[:space:]][^>]*version="\([^"]*\)".*/\1/p')
if [ -z "$ac_version" ]; then
echo "could not read the version from $ac"
exit 1
fi
if [ -z "$releases" ]; then
echo "could not read any release version from $metainfo"
exit 1
fi
# 3.49.12 -> 3,49,12,0
expected_numeric="$(echo "$versionid" | tr '.' ','),0"
fail=0
check() { # what expected actual
if [ "$2" != "$3" ]; then
echo "version.rc $1 is \"$3\", but htsglobal.h says it should be \"$2\""
echo "$1 is \"$3\", but htsglobal.h says it should be \"$2\""
fail=1
fi
}
check FILEVERSION "$expected_numeric" "$fileversion"
check PRODUCTVERSION "$expected_numeric" "$productversion"
check FileVersion "$versionid" "$rc_fileversion"
check ProductVersion "$version" "$rc_productversion"
check "version.rc FILEVERSION" "$expected_numeric" "$fileversion"
check "version.rc PRODUCTVERSION" "$expected_numeric" "$productversion"
check "version.rc FileVersion" "$versionid" "$rc_fileversion"
check "version.rc ProductVersion" "$version" "$rc_productversion"
# Signing pins the product name too.
check ProductName "HTTrack Website Copier" "$rc_productname"
check "version.rc ProductName" "HTTrack Website Copier" "$rc_productname"
check "the configure.ac AC_INIT version" "$versionid" "$ac_version"
check "the newest metainfo release" "$versionid" "$(echo "$releases" | head -n 1)"
# The check above trusts head -1, so require the list to actually descend.
echo "$releases" | awk -F'[.]' '
!/^[0-9]+\.[0-9]+\.[0-9]+$/ { print "unparsable metainfo release version: " $0; exit 1 }
{ n = $1 * 1000000000 + $2 * 1000000 + $3 }
NR > 1 && n >= prev { print "metainfo releases are not newest-first: " $0; exit 1 }
{ prev = n }
' || fail=1
[ "$fail" -eq 0 ] || exit 1
@@ -61,4 +89,4 @@ case "$out" in
;;
esac
echo "version resource agrees with htsglobal.h: $version ($expected_numeric)"
echo "version.rc, configure.ac and the metainfo agree with htsglobal.h: $version ($expected_numeric)"

View File

@@ -1,9 +1,5 @@
#!/bin/bash
#
# Keep this POSIX-portable: the harness runs it via $(BASH), which is a plain
# POSIX /bin/sh on some platforms (e.g. macOS), so avoid bashisms and GNU-only
# tool flags despite the #!/bin/bash above.
# Golden cache-format regression test (driven by 'httrack -#test=cache-golden <dir>').
#
# 01_zlib-cache.test writes the cache with the same build it reads back (a

View File

@@ -1,9 +1,5 @@
#!/bin/bash
#
# Keep this POSIX-portable: the harness runs it via $(BASH), which is a plain
# POSIX /bin/sh on some platforms (e.g. macOS), so avoid bashisms and GNU-only
# tool flags despite the #!/bin/bash above.
# Cache create/read/update logic (driven by 'httrack -#test=cache <dir>').
#
# The in-process self-test stores several hand-crafted edge entries (normal

View File

@@ -3,12 +3,7 @@
# The committed man/httrack.1 must match what man/makeman.sh produces from the
# current "httrack --help" output. This catches a --help change that was not
# followed by "make -C man regen-man".
#
# Keep this POSIX-portable: the harness runs it via $(BASH), which is a plain
# POSIX /bin/sh on some platforms (e.g. macOS), so avoid bashisms (such as
# process substitution) despite the #!/bin/bash above.
# pipefail is a bashism; keep to POSIX set flags ($(BASH) may be /bin/sh here).
set -eu
: "${top_srcdir:=..}"

View File

@@ -38,16 +38,7 @@ mkdir -p "$work/root" "$work/proj"
>"$work/server.out" 2>"$work/server.err" &
spid=$!
port=
for _ in $(seq 1 100); do
port=$(sed -n 's/^PORT \([0-9]*\)$/\1/p' "$work/server.out" 2>/dev/null || true)
test -n "$port" && break
sleep 0.1
done
test -n "$port" || {
echo "local-server.py did not announce a port" >&2
exit 1
}
port=$(discover_server_port "$work/server.out" "$spid") || exit 1
# Save path is "127.0.0.1_<port>/" + the URL path; target 998 bytes total.
prefix="127.0.0.1_${port}/"

View File

@@ -50,7 +50,31 @@ freeport() {
proxyport=$(freeport)
icpport=$(freeport)
proxytrack "127.0.0.1:$proxyport" "127.0.0.1:$icpport" "$dir/in.arc" >"$dir/pt.log" 2>&1 &
: >"$dir/pt.log" # the drainer below creates it asynchronously
# stdout to a file is fully buffered and SIGTERM never flushes it, so a leak on
# stdout would be lost; a pty line-buffers it. The exec keeps $! on proxytrack.
"$python" -c '
import os, pty, sys
master, slave = pty.openpty()
if os.fork() == 0:
os.close(slave)
with open(sys.argv[1], "wb", 0) as log:
while True:
try:
data = os.read(master, 65536)
except OSError:
break
if not data:
break
log.write(data)
os._exit(0)
os.close(master)
os.dup2(slave, 1)
os.dup2(slave, 2)
if slave > 2:
os.close(slave)
os.execvp(sys.argv[2], sys.argv[2:])
' "$dir/pt.log" proxytrack "127.0.0.1:$proxyport" "127.0.0.1:$icpport" "$dir/in.arc" &
ptpid=$!
waited=0
until grep -qE "HTTP Proxy installed on|Unable to (initialize a temporary server|create the server)" "$dir/pt.log"; do
@@ -73,8 +97,10 @@ grep -q "HTTP Proxy installed on" "$dir/pt.log" || {
}
propfind() {
curl -s -i --max-time 30 -X PROPFIND -H "Depth: 1" \
"http://127.0.0.1:$proxyport/webdav/example.com/$1"
local path=$1
shift
curl -s -i --max-time 30 -X PROPFIND -H "Depth: 1" "$@" \
"http://127.0.0.1:$proxyport/webdav/example.com/$path"
}
resp=$(propfind page.html) || {
@@ -118,4 +144,44 @@ grep -q '^HTTP/1\.[01] 207 ' <<<"$resp" || {
exit 1
}
# Leftover traces echoed the client's body and the generated response (#911).
# The padding spills the canary past any stdio buffer, newline or not.
sentinel=PT911-DAV-BODY-CANARY
pad=$(printf '%8192s' '')
resp=$(propfind "" -H 'Expect:' --data-binary "$sentinel${pad// /x}") || {
echo "FAIL: proxytrack died serving a PROPFIND carrying a body"
cat "$dir/pt.log"
exit 1
}
grep -q '^HTTP/1\.[01] 207 ' <<<"$resp" || {
echo "FAIL: expected 207 Multi-Status on the canary listing"
printf '%s\n' "$resp"
exit 1
}
# The 207 is what makes the absence below mean something: a depth-rejected
# PROPFIND is logged 403 without ever reaching the traces. It is logged after
# them, so waiting on it also fences the drainer's copy.
waited=0
until test "$(grep -ci ' \* log: HTTP [^ ]* 207 [0-9]* propfind ' "$dir/pt.log" || true)" -ge 3; do
test "$waited" -lt 50 || {
echo "FAIL: fewer than three PROPFINDs were answered 207, so the checks below prove nothing"
cat "$dir/pt.log"
exit 1
}
sleep 0.1
waited=$((waited + 1))
done
# Neither what the client sent nor the generated body is echoed anywhere; the
# two literals name the exact regression on top.
log=$(<"$dir/pt.log")
for marker in "$sentinel" 'Default Document for the Folder' 'DEBUG: DAV-DATA' '^RESPONSE:'; do
if grep -q "$marker" <<<"$log"; then
echo "FAIL: a PROPFIND echoed '$marker' into proxytrack's output"
printf '%s\n' "$log"
exit 1
fi
done
echo "OK: PROPFIND on an exact cache entry lists its default document and survives"

View File

@@ -24,7 +24,7 @@ python=$(find_python) || {
# The GUI assets asserted below have to exist, or every check goes vacuous.
for asset in server/ping.js server/style.css images/screenshot_01.jpg \
images/bg_rings.gif img/guide-droid-opt-spider.png \
images/bg_rings.svg img/fade.gif img/guide-droid-opt-spider.png \
server/div/com.httrack.WebHTTrack.metainfo.xml; do
test -f "${distdir}/html/${asset}" || fail "missing GUI asset ${asset}"
done
@@ -131,7 +131,8 @@ def check_html_headers(path):
check_html_headers("/server/index.html")
check_type("/server/ping.js", "text/javascript")
check_type("/server/style.css", "text/css")
check_type("/images/bg_rings.gif", "image/gif")
check_type("/images/bg_rings.svg", "image/svg+xml")
check_type("/img/fade.gif", "image/gif")
check_type("/img/guide-droid-opt-spider.png", "image/png")
check_type("/images/screenshot_01.jpg", "image/jpeg")
# Dots in the stem: the type comes from the last one, not the first.

View File

@@ -0,0 +1,70 @@
#!/bin/bash
# An empty backtrace() must still leave a report; a silent one reads as a
# handler that died halfway.
set -euo pipefail
ulimit -c 0 # a deliberate crash must not litter the box with cores
if [ "$(uname -s)" != "Linux" ]; then
echo "LD_PRELOAD interposition is Linux-only here, skipping" >&2
exit 77
fi
command -v httrack >/dev/null || {
echo "could not find httrack" >&2
exit 1
}
# A --disable-shared build has nothing to preload; anything else missing is a
# build problem, not a skip, or the leg below would pass vacuously.
if [ ! -r "${NOBACKTRACE_LA:-}" ]; then
echo "${NOBACKTRACE_LA:-\$NOBACKTRACE_LA} was not built" >&2
exit 1
fi
if grep -q "^dlname=''" "$NOBACKTRACE_LA"; then
echo "static-only build, skipping" >&2
exit 77
fi
if [ ! -r "${NOBACKTRACE_LIB:-}" ]; then
echo "${NOBACKTRACE_LIB:-\$NOBACKTRACE_LIB} was not built" >&2
exit 1
fi
# 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"
# Control: a build that never traces would pass the assertion below for the
# wrong reason.
rc=0
plain=$(HTTRACK_NO_SYMBOLIZE=1 httrack -#c=abort 2>&1) || rc=$? # no addr2line to wedge on
test "$rc" -eq 134 || {
echo "the plain crash exited $rc, not 134" >&2
exit 1
}
if grep -q 'No stack trace available' <<<"$plain"; then
echo "no working backtrace() in this build; skipping" >&2
exit 77
fi
rc=0
out=$(LD_PRELOAD="$NOBACKTRACE_LIB" httrack -#c=abort 2>&1) || rc=$?
test "$rc" -eq 134 || {
echo "the interposed crash exited $rc, not 134" >&2
printf '%s\n' "$out" >&2
exit 1
}
grep -q "^Caught signal 6$" <<<"$out" || {
echo "no 'Caught signal' line:" >&2
printf '%s\n' "$out" >&2
exit 1
}
grep -q 'No stack trace available: unwinding failed' <<<"$out" || {
echo "an empty backtrace left no diagnostic:" >&2
printf '%s\n' "$out" >&2
exit 1
}
grep -q "Please report the problem" <<<"$out" || {
echo "the handler stopped before its closing line:" >&2
printf '%s\n' "$out" >&2
exit 1
}

View File

@@ -0,0 +1,14 @@
#!/bin/bash
#
# A relocated install must read its own templates rather than silently falling
# back to the compiled-in defaults (#894).
set -euo pipefail
work=$(mktemp -d "${TMPDIR:-/tmp}/datadir.XXXXXX") || {
echo "no tmpdir" >&2
exit 1
}
trap 'set +e; rm -rf "${work}"' EXIT
httrack -#test=datadir "${work}"

View File

@@ -0,0 +1,40 @@
#!/bin/bash
#
# A build with a non-default --datadir installed a launcher that could not start
# (#887): the configured datadir must be searched, and after the relative entries.
set -euo pipefail
script="${abs_top_builddir:?not run under make check}/src/webhttrack"
datadir="${CONFIGURED_DATADIR:?not run under make check}"
fail() {
echo "FAIL: $*" >&2
exit 1
}
test -r "${script}" || fail "no ${script}"
line=$(grep '^SRCHDISTPATH=' "${script}") || fail "no SRCHDISTPATH in ${script}"
case "${line}" in
*'@datadir@'*) fail "@datadir@ was never substituted: ${line}" ;;
esac
# Read the list back as the shell does, so a quoting slip fails here.
BINWD='@BINWD@'
eval "${line}"
test "${#SRCHDISTPATH[@]}" -gt 3 || fail "SRCHDISTPATH is too short: ${line}"
pos=-1
for i in "${!SRCHDISTPATH[@]}"; do
test "${SRCHDISTPATH[$i]}" = "${datadir}" && pos=$i && break
done
test "${pos}" -ge 0 || fail "configured datadir ${datadir} is not in SRCHDISTPATH"
# The relative entries must still come first, or a moved tree reads the build machine's.
if [ "${SRCHDISTPATH[0]}" != "${BINWD}/../share" ] || [ "${SRCHDISTPATH[1]}" != "${BINWD}/.." ]; then
fail "the relative entries moved: ${SRCHDISTPATH[0]} ${SRCHDISTPATH[1]}"
fi
test "${pos}" -eq 2 || fail "configured datadir is at index ${pos}, expected 2"
echo "webhttrack searches the configured datadir: ${datadir}"

90
tests/146_bash-shell.test Normal file
View File

@@ -0,0 +1,90 @@
#!/bin/bash
#
# configure must hand the Makefiles a real bash (BASH_SHELL): "make deb" and the test
# driver both run bash scripts through it. Not searched into BASH, which bash presets to
# its own path, so on macOS -- where /bin/sh is a bash -- it reported /bin/sh (#895, #891).
#
# shellcheck disable=SC2016 # the probes are for the shell under test to expand
set -euo pipefail
sh=${BASH_SHELL:-}
test -n "$sh" || {
echo "BASH_SHELL is empty; tests/Makefile.am must export the configured value" >&2
exit 1
}
test -x "$sh" || {
echo "BASH_SHELL=$sh is not an executable file" >&2
exit 1
}
version=$("$sh" -c 'echo "${BASH_VERSION:-}"')
test -n "$version" || {
echo "BASH_SHELL=$sh sets no BASH_VERSION, so it is not a bash" >&2
exit 1
}
# sh-mode bash sets BASH_VERSION too, so the version alone proves nothing.
opts=$("$sh" -c 'echo ":${SHELLOPTS:-}:"')
case "$opts" in
*:posix:*)
echo "BASH_SHELL=$sh is a bash in POSIX sh-mode" >&2
exit 1
;;
esac
# The test helpers use process substitution, which macOS's bash-3.2-as-sh rejects.
out=$("$sh" -c 'cat <(echo procsub)' 2>&1) || {
echo "BASH_SHELL=$sh cannot run a process substitution: $out" >&2
exit 1
}
test "$out" = procsub || {
echo "expected 'procsub' from BASH_SHELL=$sh, got: $out" >&2
exit 1
}
# Whatever configure picks, an absolute BASH_SHELL from the user must win over the search.
configure="${abs_top_srcdir:-}/configure"
test -r "$configure" || {
echo "no configure script at $configure; tests/Makefile.am must export abs_top_srcdir" >&2
exit 1
}
help=$(bash "$configure" --help)
grep -q '^ *BASH_SHELL ' <<<"$help" || {
echo "configure --help does not advertise BASH_SHELL (AC_ARG_VAR missing)" >&2
exit 1
}
tmp=$(mktemp -d)
trap 'set +e; rm -rf "$tmp"' EXIT
mkdir "$tmp/src" "$tmp/bld"
ln -s "$sh" "$tmp/mybash"
# Symlink farm, not the real srcdir: an in-tree config.status makes autoconf refuse it.
for f in "$abs_top_srcdir"/*; do
case "${f##*/}" in
config.status | config.log | config.h | stamp-h1 | Makefile) continue ;;
esac
ln -s "$f" "$tmp/src/"
done
# Prefer the Makefile: it proves the value that reaches $(BASH_SHELL), not just the macro's
# decision. configure may die on a library check before writing one, so fall back to the
# trace it printed earlier.
(cd "$tmp/bld" && BASH_SHELL="$tmp/mybash" bash "$tmp/src/configure" --disable-https >conf.log 2>&1) || true
if test -f "$tmp/bld/Makefile"; then
got=$(sed -n 's/^BASH_SHELL = //p' "$tmp/bld/Makefile")
from="Makefile"
else
got=$(sed -n 's/^checking for bash\.\.\. //p' "$tmp/bld/conf.log")
got=${got#"(cached) "}
from="trace"
fi
test "$got" = "$tmp/mybash" || {
echo "configure discarded BASH_SHELL=$tmp/mybash, $from has: ${got:-<nothing>}" >&2
tail -20 "$tmp/bld/conf.log" >&2
exit 1
}
echo "configured bash is $sh ($version)"

View File

@@ -0,0 +1,162 @@
#!/bin/bash
#
# escapexml() expands '&' fivefold, so an unauthenticated PROPFIND path of
# escaped ampersands used to overflow the fixed DAV item reserve (#836). The
# Depth: 1 listing at the end covers the enumeration branch's own item URLs.
set -euo pipefail
: "${top_srcdir:=..}"
# shellcheck source=tests/testlib.sh
. "$top_srcdir/tests/testlib.sh"
python=$(find_python) || {
echo "python3 missing, skipping"
exit 77
}
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=
cleanup() {
stop_server "$ptpid"
rm -rf "$dir"
}
trap 'set +e; cleanup' EXIT
trap 'set +e; cleanup; exit 1' INT TERM HUP
printf 'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 5\r\n\r\n' >"$dir/hdr"
printf 'hello' >"$dir/body"
alen=$(($(wc -c <"$dir/hdr") + $(wc -c <"$dir/body")))
{
printf 'filedesc://t.arc 0.0.0.0 20250101000000 text/plain 200 - - 0 t.arc 9\n'
printf '2 0 test\n'
printf '\n\n'
printf 'http://example.com/page.html 0.0.0.0 20250101000000 text/html 200 - - 0 t.arc %d\n' "$alen"
cat "$dir/hdr" "$dir/body"
# a record begins on the newline following the previous one's data
printf '\n'
# gives the Depth: 1 listing below a child directory as well as a child file
printf 'http://example.com/sub/deep.html 0.0.0.0 20250101000000 text/html 200 - - 0 t.arc %d\n' "$alen"
cat "$dir/hdr" "$dir/body"
} >"$dir/in.arc"
freeport() {
"$python" -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close()'
}
proxyport=$(freeport)
icpport=$(freeport)
proxytrack "127.0.0.1:$proxyport" "127.0.0.1:$icpport" "$dir/in.arc" >"$dir/pt.log" 2>&1 &
ptpid=$!
waited=0
until grep -qE "HTTP Proxy installed on|Unable to (initialize a temporary server|create the server)" "$dir/pt.log"; do
kill -0 "$ptpid" 2>/dev/null || {
echo "FAIL: proxytrack exited before listening"
cat "$dir/pt.log"
exit 1
}
test "$waited" -lt 50 || {
echo "FAIL: proxytrack never announced its listen port"
exit 1
}
sleep 0.1
waited=$((waited + 1))
done
grep -q "HTTP Proxy installed on" "$dir/pt.log" || {
echo "FAIL: proxytrack failed to bind"
cat "$dir/pt.log"
exit 1
}
propfind() {
curl -s -i --max-time 30 -X PROPFIND -H "Depth: 0" \
"http://127.0.0.1:$proxyport/webdav/x/$1"
}
# 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)")
resp=$(propfind "$amps") || {
echo "FAIL: proxytrack died on an escapexml-amplified PROPFIND"
cat "$dir/pt.log"
exit 1
}
grep -q '^HTTP/1\.[01] 207 ' <<<"$resp" || {
echo "FAIL: expected 207 Multi-Status for the amplified path"
head -c 512 <<<"$resp"
exit 1
}
# Count what came back: a clipped path answers 207 too. The run appears in
# the href and again in the displayname.
got=$({ grep -o '&amp;' <<<"$resp" || true; } | wc -l)
test "$got" -eq 1800 || {
echo "FAIL: 900 ampersands went in, $got came back escaped, wanted 1800"
exit 1
}
# Unescaped, so the bound is shown to be on the written length, not on '&'.
plain=$("$python" -c "print('a' * 900)")
resp=$(propfind "$plain") || {
echo "FAIL: proxytrack died on a 900-byte plain PROPFIND path"
cat "$dir/pt.log"
exit 1
}
grep -q "<href>/webdav/x/$plain</href>" <<<"$resp" || {
echo "FAIL: a 900-byte path did not come back whole"
exit 1
}
# Depth: 1 is the only route into the enumeration branch, which builds each
# item URL from the prefix and the child name. A child directory is enumerated
# with a trailing '/' that has to come back off, or its name is lost and the
# entry lists as the folder's default document.
resp=$(curl -s -i --max-time 30 -X PROPFIND -H "Depth: 1" \
"http://127.0.0.1:$proxyport/webdav/example.com/") || {
echo "FAIL: proxytrack died on a Depth: 1 listing"
cat "$dir/pt.log"
exit 1
}
grep -q '^HTTP/1\.[01] 207 ' <<<"$resp" || {
echo "FAIL: expected 207 Multi-Status for the Depth: 1 listing"
printf '%s\n' "$resp"
exit 1
}
for want in '<href>/webdav/example\.com</href>' \
'<href>/webdav/example\.com/page\.html</href>' \
'<href>/webdav/example\.com/sub</href>' \
'<displayname>sub</displayname>'; do
grep -q "$want" <<<"$resp" || {
echo "FAIL: the Depth: 1 listing is missing $want"
printf '%s\n' "$resp"
exit 1
}
done
responses=$(grep -c '<response ' <<<"$resp" || true)
test "$responses" -eq 3 || {
echo "FAIL: expected the root, the file and the directory, got $responses responses"
printf '%s\n' "$resp"
exit 1
}
# The overflow killed the listener, so a later request is the liveness proof.
resp=$(propfind "") || {
echo "FAIL: proxytrack died before the follow-up listing"
cat "$dir/pt.log"
exit 1
}
grep -q '^HTTP/1\.[01] 207 ' <<<"$resp" || {
echo "FAIL: expected 207 Multi-Status on the follow-up listing"
printf '%s\n' "$resp"
exit 1
}
echo "OK: an escapexml-amplified PROPFIND path is served whole and does not overflow"

14
tests/148_engine-spoolname.test Executable file
View File

@@ -0,0 +1,14 @@
#!/bin/bash
#
# A frozen backlog slot must spool outside the mirror namespace, or a site
# serving <path>.tmp has its own copy truncated and then unlinked (#859).
set -euo pipefail
work=$(mktemp -d "${TMPDIR:-/tmp}/spoolname.XXXXXX") || {
echo "no tmpdir" >&2
exit 1
}
trap 'set +e; rm -rf "${work}"' EXIT
httrack -#test=spoolname "${work}"

View File

@@ -0,0 +1,30 @@
#!/bin/bash
#
# A trailer section after the terminating chunk (RFC 9112 7.1.2, #855) is
# discarded, bounded by HTS_LINE_BLOCK_SIZE (huge.html), and junk where a data
# chunk's own CRLF belongs stays a framing error (bogus.html). eof.html pins the
# deliberate leniency: past a complete body, an unterminated section still lands.
set -euo pipefail
: "${top_srcdir:=..}"
bash "$top_srcdir/tests/local-crawl.sh" \
--log-not-found 'Invalid chunk.*chunktrail/(one|many|none|file|eof)' \
--log-not-found 'illegal chunk CRLF.*chunktrail/(one|many|none|file|huge|eof)' \
--file-matches 'chunktrail/one.html' 'CHUNKTRAIL-ONE' \
--file-matches 'chunktrail/many.html' 'CHUNKTRAIL-MANY' \
--file-matches 'chunktrail/none.html' 'CHUNKTRAIL-NONE' \
--file-matches 'chunktrail/file.bin' 'CHUNKTRAIL-BIN' \
--file-min-bytes 'chunktrail/file.bin' 16384 \
--file-matches 'chunktrail/eof.html' 'CHUNKTRAIL-EOF' \
--cache-found '/chunktrail/one.html' \
--cache-found '/chunktrail/many.html' \
--cache-found '/chunktrail/file.bin' \
--log-found 'Interrupted transfer.*chunktrail/huge\.html' \
--not-found 'chunktrail/huge.html' \
--cache-not-found '/chunktrail/huge.html' \
--log-found 'illegal chunk CRLF.*chunktrail/bogus\.html' \
--not-found 'chunktrail/bogus.html' \
--cache-not-found '/chunktrail/bogus.html' \
httrack 'BASEURL/chunktrail/index.html'

View File

@@ -0,0 +1,25 @@
#!/bin/bash
#
# StringSprintf writes the terminator at buffer[ret], so a `ret <= capacity`
# guard overflows by one byte whenever the output exactly fills the capacity
# (#836). The proxytrack crawl tests never land on a capacity boundary.
set -euo pipefail
: "${top_srcdir:=..}"
# Resolve httrack before any cd: PATH may carry a build-relative entry.
bin=$(command -v httrack) || {
echo "FAIL: httrack not found on PATH" >&2
exit 1
}
fail() {
echo "FAIL: $1" >&2
exit 1
}
out=$("$bin" -#test=strsprintf) || fail "httrack -#test=strsprintf exited non-zero: $out"
test "$out" = "strsprintf self-test: OK" || fail "expected 'strsprintf self-test: OK', got: $out"
exit 0

View File

@@ -0,0 +1,166 @@
#!/bin/bash
#
# An absolute BASH_SHELL is taken verbatim by AC_PATH_PROGS and a relative one is dropped
# for the search result, so configure has to check what it ends up with: BASH_SHELL=/bin/sh
# otherwise puts #895 back and only shows up at "make check" or "make deb" (#908).
set -euo pipefail
sh=${BASH_SHELL:-}
test -n "$sh" || {
echo "BASH_SHELL is empty; tests/Makefile.am must export the configured value" >&2
exit 1
}
configure="${abs_top_srcdir:-}/configure"
test -r "$configure" || {
echo "no configure script at $configure; tests/Makefile.am must export abs_top_srcdir" >&2
exit 1
}
tmp=$(mktemp -d)
cleanup() {
# O_RDWR never blocks, and it releases whatever is left stuck opening the FIFO for read.
exec 9<>"$tmp/fifo" && exec 9>&-
rm -rf "$tmp"
}
trap 'set +e; cleanup' EXIT
trap 'set +e; cleanup; exit 1' HUP INT TERM
# Symlink farm, not the real srcdir: an in-tree config.status makes autoconf refuse it.
mkdir "$tmp/src"
for f in "$abs_top_srcdir"/*; do
case "${f##*/}" in
config.status | config.log | config.h | stamp-h1 | Makefile) continue ;;
esac
ln -s "$f" "$tmp/src/"
done
mkdir "$tmp/notexec" "$tmp/with space" "$tmp/fakebin"
: >"$tmp/notexec/bash"
chmod 644 "$tmp/notexec/bash"
ln -s "$sh" "$tmp/mybash"
ln -s "$sh" "$tmp/with space/bash"
ln -s "$sh" "$tmp/sh" # bash invoked as "sh" enters POSIX mode
# A "bash" the PATH search will find first, and that answers -c like any other shell.
printf '#!/bin/sh\nexec /bin/sh "$@"\n' >"$tmp/fakebin/bash"
chmod 755 "$tmp/fakebin/bash"
# Executable to "test -x", but bash blocks in open() on one it failed to exec.
mkfifo "$tmp/fifo"
chmod 755 "$tmp/fifo"
n=0
status=0
log=
rundir=
run() { # run <label> <env argument>...
local label=$1
shift
n=$((n + 1))
rundir="$tmp/run$n"
mkdir "$rundir"
status=0
# Capped: configure executes the candidate, and a hang wedges "make check" with no output
# at all. Polled, not a backgrounded "sleep" watchdog, which outlives the run it guards.
(cd "$rundir" && env "$@" bash "$tmp/src/configure" --disable-https) \
>"$rundir/log" 2>&1 &
local pid=$! waited=0
while test "$waited" -lt 300 && kill -0 "$pid" 2>/dev/null; do
sleep 1
waited=$((waited + 1))
done
if kill -0 "$pid" 2>/dev/null; then
kill -9 "$pid" 2>/dev/null
echo "configure did not return within ${waited}s for $label" >&2
tail -5 "$rundir/log" >&2
exit 1
fi
wait "$pid" || status=$?
log=$(cat "$rundir/log")
echo "run $n ($label): exit $status"
}
reject() { # reject <label> <expected message> <env argument>...
local label=$1 want=$2
shift 2
run "$label" "$@"
test "$status" -ne 0 || {
echo "configure accepted $label" >&2
exit 1
}
grep -q "$want" <<<"$log" || {
echo "$label rejected without '$want':" >&2
tail -5 <<<"$log" >&2
exit 1
}
}
# accept <label> <expected $(BASH_SHELL), "" for any> <expected message, "" for none> <env argument>...
accept() {
local label=$1 path=$2 want=$3
shift 3
run "$label" "$@"
test "$status" -eq 0 || {
echo "configure rejected $label (exit $status):" >&2
tail -10 <<<"$log" >&2
exit 1
}
got=$(sed -n 's/^BASH_SHELL = //p' "$rundir/Makefile")
test -n "$got" || {
echo "$label configured, but the Makefile carries no BASH_SHELL" >&2
exit 1
}
if test -n "$path" && test "$got" != "$path"; then
echo "$label reached the Makefile as $got" >&2
exit 1
fi
if test -n "$want"; then
grep -q "$want" <<<"$log" || {
echo "$label configured without '$want':" >&2
tail -5 <<<"$log" >&2
exit 1
}
fi
}
reject relative 'BASH_SHELL must be an absolute path' BASH_SHELL=relbash
notreg='is not an executable regular file'
reject missing "$notreg" "BASH_SHELL=$tmp/missing/bash"
reject non-executable "$notreg" "BASH_SHELL=$tmp/notexec/bash"
# "test -x" passes a FIFO, and configure then hangs forever instead of rejecting it (#922).
reject fifo "$notreg" "BASH_SHELL=$tmp/fifo"
reject sh-mode 'is a bash in POSIX sh-mode' "BASH_SHELL=$tmp/sh"
# The #908 case: dash on Linux, bash in sh-mode on macOS, unusable either way. The spoofed
# BASH_VERSION is what an ordinary shell echoes straight back, so it cannot be the probe.
reject /bin/sh 'BASH_SHELL=/bin/sh is ' BASH_SHELL=/bin/sh BASH_VERSION=9.9
# make splits the value on whitespace, cuts it at a '#' and expands a '$', and no Makefile
# quotes $(BASH_SHELL).
meta='must not contain shell or make metacharacters'
reject space "$meta" "BASH_SHELL=$tmp/with space/bash"
reject semicolon "$meta" 'BASH_SHELL=/opt/a;b/bash'
# shellcheck disable=SC2016 # the '$' has to reach configure unexpanded
reject dollar "$meta" 'BASH_SHELL=/opt/a$b/bash'
reject hash "$meta" 'BASH_SHELL=/opt/a#b/bash'
# An environment in POSIX mode leaves no path that could pass, so the message must blame it
# and not the shell.
reject posix-env 'POSIXLY_CORRECT or SHELLOPTS' "BASH_SHELL=$sh" POSIXLY_CORRECT=1
# Both at once: clearing the variable would still leave a bash invoked as sh, so the second
# probe has to decide which one to blame instead of reading the environment.
reject sh-mode-in-posix-env "BASH_SHELL=$tmp/sh is a bash in POSIX sh-mode" \
"BASH_SHELL=$tmp/sh" POSIXLY_CORRECT=1
if grep -q 'POSIXLY_CORRECT or SHELLOPTS' <<<"$log"; then
echo "a bash invoked as sh was blamed on the environment:" >&2
tail -5 <<<"$log" >&2
exit 1
fi
accept override "$tmp/mybash" '' "BASH_SHELL=$tmp/mybash"
accept empty '' '' BASH_SHELL=
# No override: an unusable bash is a warning, so a box that has none still builds.
accept searched "$tmp/fakebin/bash" 'no usable bash found' -u BASH_SHELL "PATH=$tmp/fakebin:$PATH"
accept searched-posix-env '' 'POSIXLY_CORRECT or SHELLOPTS' -u BASH_SHELL POSIXLY_CORRECT=1
echo "configure validated $n BASH_SHELL values"

View File

@@ -0,0 +1,47 @@
#!/bin/bash
#
# A String that cannot grow must stop the process loudly rather than carry on
# with a NULL buffer and a capacity already bumped. assert() is compiled out of
# the MSVC Release build, so the check cannot lean on it (#915). The failure is
# injected through a realloc stub: asking a real allocator for a size it should
# refuse is a guess about the machine, not a test.
set -euo pipefail
ulimit -c 0 # a deliberate abort must not litter the box with cores
fail() {
echo "FAIL: $1" >&2
exit 1
}
# Unset means this is not an automake run (the Windows job runs the scripts
# directly), so the helper does not exist. Set but missing is a build problem,
# not a skip, or the assertions below would pass vacuously.
if [ -z "${STRINGOOM_BIN:-}" ]; then
echo "STRINGOOM_BIN unset, no automake environment, skipping" >&2
exit 77
fi
[ -r "$STRINGOOM_BIN" ] || fail "$STRINGOOM_BIN was not built"
# Control first: with the stub allocating for real, growth must not reach the
# failure path at all, and the capacity it announces must be writable to the
# last byte. Then the three failure shapes, and the growers inside the header.
for mode in grow hook keep sprintf buffn; do
out=$("$STRINGOOM_BIN" "$mode") || fail "$mode case exited non-zero: $out"
test "$out" = "$mode: OK" || fail "expected '$mode: OK', got: $out"
done
# The shipped handler prints and aborts, so don't let set -e trip on it.
err=$("$STRINGOOM_BIN" abort 2>&1) || true
case "$err" in
*"NOT aborted"*)
fail "the failed allocation was not caught: $err"
;;
*"String: out of memory"*) ;;
*)
fail "expected the String out-of-memory abort, got: $err"
;;
esac
exit 0

View File

@@ -0,0 +1,227 @@
#!/bin/bash
#
# coucal logs a per-table statistics summary when a table is deleted, and with
# no handler installed it writes it itself, prefixed with the table address.
# proxytrack builds and drops one table per WebDAV enumeration, so every
# unauthenticated PROPFIND dumped a line to the operator's console (#918).
set -euo pipefail
: "${top_srcdir:=..}"
# shellcheck source=tests/testlib.sh
. "$top_srcdir/tests/testlib.sh"
python=$(find_python) || {
echo "python3 missing, skipping"
exit 77
}
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)
quietpid=
loudpid=
cleanup() {
stop_server "$quietpid"
stop_server "$loudpid"
rm -rf "$dir"
}
trap 'set +e; cleanup' EXIT
trap 'set +e; cleanup; exit 1' INT TERM HUP
printf 'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 5\r\n\r\n' >"$dir/hdr"
printf 'hello' >"$dir/body"
alen=$(($(wc -c <"$dir/hdr") + $(wc -c <"$dir/body")))
{
printf 'filedesc://t.arc 0.0.0.0 20250101000000 text/plain 200 - - 0 t.arc 9\n'
printf '2 0 test\n'
printf '\n\n'
printf 'http://example.com/page.html 0.0.0.0 20250101000000 text/html 200 - - 0 t.arc %d\n' "$alen"
cat "$dir/hdr" "$dir/body"
} >"$dir/in.arc"
freeport() {
"$python" -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close()'
}
# stdout to a file is fully buffered and SIGTERM never flushes it, so a leak on
# stdout would be lost; a pty line-buffers it. The exec keeps $! on proxytrack.
pty_exec='
import os, pty, sys
master, slave = pty.openpty()
if os.fork() == 0:
os.close(slave)
with open(sys.argv[1], "wb", 0) as log:
while True:
try:
data = os.read(master, 65536)
except OSError:
break
if not data:
break
log.write(data)
open(sys.argv[1] + ".done", "wb").close()
os._exit(0)
os.close(master)
os.dup2(slave, 1)
os.dup2(slave, 2)
if slave > 2:
os.close(slave)
os.execvp(sys.argv[2], sys.argv[2:])
'
wait_listen() { # log pid
local waited=0
until grep -qE "HTTP Proxy installed on|Unable to (initialize a temporary server|create the server)" "$1"; do
kill -0 "$2" 2>/dev/null || {
echo "FAIL: proxytrack exited before listening"
cat "$1"
exit 1
}
test "$waited" -lt 50 || {
echo "FAIL: proxytrack never announced its listen port"
exit 1
}
sleep 0.1
waited=$((waited + 1))
done
grep -q "HTTP Proxy installed on" "$1" || {
echo "FAIL: proxytrack failed to bind"
cat "$1"
exit 1
}
}
propfind() { # port path
curl -s -i --max-time 30 -X PROPFIND -H "Depth: 1" \
"http://127.0.0.1:$1/webdav/example.com/$2"
}
# Whatever the process wrote once serving; the banner ends on the PID line. A
# missing PID line yields nothing, which the 207 count below then fails on.
request_log() { # log
sed -n '/^PID=/,$p' "$1" | tail -n +2 | tr -d '\r'
}
# The drainer is proxytrack's child, not this shell's, so it cannot be waited
# on; it marks the log once the pty has closed and the copy is complete.
wait_drained() { # log
local waited=0
until test -e "$1.done"; do
test "$waited" -lt 100 || {
echo "FAIL: the pty drainer never finished copying the output"
exit 1
}
sleep 0.1
waited=$((waited + 1))
done
}
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" &
quietpid=$!
wait_listen "$dir/quiet.log" "$quietpid"
# Both on one connection: the access log is written before send(), and only
# proxytrack's keep-alive loop orders anything written after it, since the
# second request is not read until the first one's teardown has run.
resp=$(curl -s -i --max-time 30 -X PROPFIND -H "Depth: 1" \
"http://127.0.0.1:$quietport/webdav/example.com/" \
"http://127.0.0.1:$quietport/webdav/example.com/page.html") || {
echo "FAIL: the PROPFINDs did not answer"
cat "$dir/quiet.log"
exit 1
}
served=$(grep -c '^HTTP/1\.[01] 207 ' <<<"$resp" || true)
test "$served" -eq 2 || {
echo "FAIL: expected two 207 Multi-Status replies, got $served"
printf '%s\n' "$resp"
exit 1
}
# The collection and its entry, then that entry and its default document. An
# empty listing builds no table and would make the check below vacuous.
listed=$(grep -c '<response ' <<<"$resp" || true)
test "$listed" -eq 4 || {
echo "FAIL: the two listings enumerated $listed entries, wanted 4"
printf '%s\n' "$resp"
exit 1
}
# The enumeration is what builds the table, so the served 207s are what make
# the check below mean anything.
waited=0
until test "$(request_log "$dir/quiet.log" | grep -ci ' \* log: HTTP [^ ]* 207 [0-9]* propfind ' || true)" -ge 2; do
test "$waited" -lt 50 || {
echo "FAIL: fewer than two PROPFINDs were answered 207, so the check below proves nothing"
cat "$dir/quiet.log"
exit 1
}
sleep 0.1
waited=$((waited + 1))
done
# Only the access log is written before send(), so read the capture once
# proxytrack is gone and the drainer has copied all of it: whatever a teardown
# or keep-alive path writes has no ordering against the fence above, and no
# sleep is short enough to be sound or long enough to be reliable.
stop_server "$quietpid"
quietpid=
wait_drained "$dir/quiet.log"
# The property is that serving adds nothing but the access log, so whitelist
# what a line may be; naming strings that must be absent would pass any
# reworded or differently shaped leak.
extra=$(request_log "$dir/quiet.log" | grep -vE '^( \* log: (HTTP|ICP) .*)?$' || true)
test -z "$extra" || {
echo "FAIL: serving a PROPFIND wrote this to proxytrack's console:"
printf '%s\n' "$extra"
exit 1
}
# HTS_LOG brings the summary back, so the silence above is a routed handler and
# not a deleted message, and the grep that asserted it does match when fed one.
loudport=$(freeport)
: >"$dir/loud.log"
HTS_LOG=1 "$python" -c "$pty_exec" "$dir/loud.log" \
proxytrack "127.0.0.1:$loudport" "127.0.0.1:$(freeport)" "$dir/in.arc" &
loudpid=$!
wait_listen "$dir/loud.log" "$loudpid"
resp=$(propfind "$loudport" "") || {
echo "FAIL: PROPFIND under HTS_LOG did not answer"
cat "$dir/loud.log"
exit 1
}
grep -q '^HTTP/1\.[01] 207 ' <<<"$resp" || {
echo "FAIL: expected 207 Multi-Status under HTS_LOG"
printf '%s\n' "$resp"
exit 1
}
waited=0
until grep -qE 'hashtable .*summary:' "$dir/loud.log"; do
test "$waited" -lt 50 || {
echo "FAIL: HTS_LOG=1 did not restore the hashtable summary"
cat "$dir/loud.log"
exit 1
}
sleep 0.1
waited=$((waited + 1))
done
# Pin the routed shape whole: a severity, then the message, and nothing in
# between where the table address used to be.
grep -qE '^ \* debug: hashtable "[^"]*" summary:' "$dir/loud.log" || {
echo "FAIL: the summary under HTS_LOG did not come through proxytrack's log"
cat "$dir/loud.log"
exit 1
}
echo "OK: PROPFIND leaves no hashtable statistics on the console, HTS_LOG restores them"

View File

@@ -7,7 +7,8 @@ set -euo pipefail
testdir=$(cd "$(dirname "$0")" && pwd)
distdir=$(cd "${testdir}/.." && pwd)
script="${distdir}/src/webhttrack"
# The .in, not the generated script: only SRCHDISTPATH differs, and this stays in srcdir.
script="${distdir}/src/webhttrack.in"
fail() {
echo "FAIL: $*" >&2

View File

@@ -43,6 +43,16 @@ if grep -q 'No stack trace available on this OS' "$out"; then
echo "no backtrace() on this platform; skipping" >&2
exit 77
fi
# 32-bit ARM is the one target whose toolchain may still refuse to unwind;
# anywhere else an empty trace is the regression this test exists to catch.
case "$(uname -m)" in
arm | armv*)
if grep -q 'No stack trace available' "$out"; then
echo "no unwind tables in this ARM build; skipping" >&2
exit 77
fi
;;
esac
# Unconditional and first: symbolizing must never cost the raw trace.
grep -q "$rawframe" "$out" || {
echo "raw module+offset frames are gone:" >&2

View File

@@ -87,12 +87,31 @@ def get(path):
return urllib.request.urlopen(url + path, timeout=20).read()
def status(path):
try:
return urllib.request.urlopen(url + path, timeout=20).getcode()
except urllib.error.HTTPError as e:
return e.code
# No crawl has run, so no commandRunning/commandEnd override swaps the page out.
finished = get("/server/finished.html").decode("latin-1")
# Control: an empty or unrendered page would pass "no file:" on its own.
check("HTTrack Website Copier" in finished, "finished.html rendered")
check("file://" not in finished, "finished.html has no file: link")
# Every image a GUI page names must be served. The masthead is referenced from 23
# of them, so one renamed asset breaks the lot and nothing else here would notice.
page = get("/server/step2.html").decode("latin-1")
srcs = [s for s in re.findall(r'<img[^>]+src="([^"]+)"', page)
if not s.startswith(("http:", "https:", "data:"))]
check(len(srcs) > 0, "step2.html names local images (got %d)" % len(srcs))
for src in sorted(set(srcs)):
got = status(urllib.parse.urljoin("/server/", src))
check(got == 200, "image %s is served (got %d)" % (src, got))
# Control: the loop above proves nothing unless a missing asset really 404s.
check(status("/server/images/absent.svg") == 404, "a missing image 404s")
# Control: /website/ 404s until a project is set, so the fetches below are real.
try:
get("/website/probe.txt")

View File

@@ -8,6 +8,8 @@ set -euo pipefail
testdir=$(cd "$(dirname "$0")" && pwd)
distdir=${top_srcdir:-$(cd "${testdir}/.." && pwd)}
distdir=$(cd "${distdir}" && pwd)
# shellcheck source=tests/testlib.sh
. "${testdir}/testlib.sh"
fail() {
echo "FAIL: $*" >&2
@@ -148,12 +150,8 @@ grep -qi '^Content-type: text/html' "${hdr}" ||
clog="${work}/content.log"
python3 "${testdir}/local-server.py" --root "${work}" >"${clog}" 2>&1 &
csrv=$!
for _ in $(seq 1 40); do
cport=$(sed -n 's/^PORT //p' "${clog}") && test -n "${cport}" && break
kill -0 "${csrv}" 2>/dev/null || break
sleep 0.25
done
test -n "${cport:-}" || fail "content server did not come up: $(cat "${clog}")"
cport=$(discover_server_port "${clog}" "${csrv}") ||
fail "content server did not come up"
post "${port}" "sid=${sid}" "path=${work}" projname=crawl winprofile=x \
command_do=start \

View File

@@ -4,7 +4,7 @@ include $(srcdir)/tests-list.mk
# Committed binary fixture read by 01_zlib-cache-golden.test. List it
# explicitly: automake does not expand wildcards in EXTRA_DIST, so a glob would
# silently drop it from the dist tarball and break "make distcheck".
EXTRA_DIST = $(TESTS) renamefail.c threadattrfail.c crawl-test.sh run-all-tests.sh check-network.sh \
EXTRA_DIST = $(TESTS) renamefail.c threadattrfail.c nobacktrace.c crawl-test.sh run-all-tests.sh check-network.sh \
proxy-https-server.py socks5-server.py proxy-connect-server.py \
proxytestlib.py tls-stall-server.py warc-validate.py wacz-validate.py \
pty-resize.py test-timeout.sh \
@@ -24,15 +24,23 @@ TESTS_ENVIRONMENT += HTTPS_SUPPORT=$(HTTPS_SUPPORT)
TESTS_ENVIRONMENT += BROTLI_ENABLED=$(BROTLI_ENABLED)
TESTS_ENVIRONMENT += ZSTD_ENABLED=$(ZSTD_ENABLED)
TESTS_ENVIRONMENT += V6_SUPPORT=$(V6_SUPPORT)
# Asserted by 146_bash-shell.test.
TESTS_ENVIRONMENT += BASH_SHELL=$(BASH_SHELL)
TESTS_ENVIRONMENT += top_srcdir=$(top_srcdir)
TESTS_ENVIRONMENT += abs_top_srcdir=$(abs_top_srcdir)
TESTS_ENVIRONMENT += abs_top_builddir=$(abs_top_builddir)
TESTS_ENVIRONMENT += CONFIGURED_DATADIR=$(datadir)
TESTS_ENVIRONMENT += RENAMEFAIL_LA=$(abs_builddir)/librenamefail.la
TESTS_ENVIRONMENT += RENAMEFAIL_LIB=$(abs_builddir)/$(LT_CV_OBJDIR)/librenamefail.so
TESTS_ENVIRONMENT += THREADATTRFAIL_LA=$(abs_builddir)/libthreadattrfail.la
TESTS_ENVIRONMENT += THREADATTRFAIL_LIB=$(abs_builddir)/$(LT_CV_OBJDIR)/libthreadattrfail.so
TESTS_ENVIRONMENT += NOBACKTRACE_LA=$(abs_builddir)/libnobacktrace.la
TESTS_ENVIRONMENT += NOBACKTRACE_LIB=$(abs_builddir)/$(LT_CV_OBJDIR)/libnobacktrace.so
TESTS_ENVIRONMENT += STRINGOOM_BIN=$(abs_builddir)/stringoom$(EXEEXT)
# rename() interposer for 01_engine-renameover.test; -rpath is what makes
# libtool build a shared module rather than a static-only convenience library.
check_LTLIBRARIES = librenamefail.la libthreadattrfail.la
check_LTLIBRARIES = librenamefail.la libthreadattrfail.la libnobacktrace.la
librenamefail_la_SOURCES = renamefail.c
librenamefail_la_LDFLAGS = -module -avoid-version -rpath $(abs_builddir)
librenamefail_la_LIBADD = $(DL_LIBS)
@@ -42,6 +50,16 @@ libthreadattrfail_la_SOURCES = threadattrfail.c
libthreadattrfail_la_LDFLAGS = -module -avoid-version -rpath $(abs_builddir)
libthreadattrfail_la_LIBADD = $(DL_LIBS)
# backtrace() interposer for 143_engine-backtrace-empty.test
libnobacktrace_la_SOURCES = nobacktrace.c
libnobacktrace_la_LDFLAGS = -module -avoid-version -rpath $(abs_builddir)
# realloc stub for 152_engine-string-oom.test; links nothing, so it stays a
# plain binary rather than a libtool wrapper script.
check_PROGRAMS = stringoom
stringoom_SOURCES = stringoom.c
stringoom_CPPFLAGS = -I$(top_srcdir)/src
TEST_EXTENSIONS = .test
# Run each .test through bash instead of execve()ing it. This lets "make check"
# work when the source tree sits on a noexec filesystem (the driver would
@@ -52,5 +70,5 @@ TEST_EXTENSIONS = .test
# named, with diagnostics, rather than hang the job until CI cancels it -- a
# cancelled step keeps neither its log nor its artifacts, so today a hang tells
# us nothing at all. HTTRACK_TEST_TIMEOUT overrides the budget; 0 disables it.
TEST_LOG_COMPILER = $(BASH) $(srcdir)/test-timeout.sh
TEST_LOG_COMPILER = $(BASH_SHELL) $(srcdir)/test-timeout.sh
CLEANFILES = check-network_sh.cache

View File

@@ -20,6 +20,7 @@ import hashlib
import os
import re
import socket
import socketserver
import struct
import sys
import time
@@ -1691,6 +1692,112 @@ class Handler(SimpleHTTPRequestHandler):
pass
self.close_connection = True
# #855: a trailer section after the terminating zero-length chunk.
def send_chunked_trailed(self, body, trailers, ctype="text/html; charset=utf-8"):
self.protocol_version = "HTTP/1.1"
self.send_response(200)
self.send_header("Content-Type", ctype)
self.send_header("Transfer-Encoding", "chunked")
self.send_header("Connection", "close")
self.end_headers()
if self.command == "HEAD":
return
try:
half = len(body) // 2
for piece in (body[:half], body[half:]):
self.wfile.write(b"%X\r\n" % len(piece) + piece + b"\r\n")
self.wfile.write(
b"0\r\n" + b"".join(t + b"\r\n" for t in trailers) + b"\r\n"
)
self.wfile.flush()
except OSError:
pass
self.close_connection = True
def route_chunktrail_index(self):
self.send_html(
'\t<a href="one.html">one</a>\n'
'\t<a href="many.html">many</a>\n'
'\t<a href="none.html">none</a>\n'
'\t<a href="huge.html">huge</a>\n'
'\t<a href="bogus.html">bogus</a>\n'
'\t<a href="eof.html">eof</a>\n'
'\t<a href="file.bin">file</a>\n'
)
def route_chunktrail_one(self):
self.send_chunked_trailed(
b"<html><body><p>CHUNKTRAIL-ONE</p></body></html>", [b"X-Foo: bar"]
)
# Spans several reads: longer than the reader's 256 bytes per call.
def route_chunktrail_many(self):
self.send_chunked_trailed(
b"<html><body><p>CHUNKTRAIL-MANY</p></body></html>",
[b"X-Field-%02d: %s" % (n, b"v" * 40) for n in range(20)],
)
# Control: same body, no trailer section.
def route_chunktrail_none(self):
self.send_chunked_trailed(
b"<html><body><p>CHUNKTRAIL-NONE</p></body></html>", []
)
# Past HTS_LINE_BLOCK_SIZE: the discard stays bounded, so the transfer drops.
def route_chunktrail_huge(self):
self.send_chunked_trailed(
b"<html><body><p>CHUNKTRAIL-HUGE</p></body></html>",
[b"X-Bloat-%04d: %s" % (n, b"w" * 100) for n in range(160)],
)
# Only the terminating chunk opens the section, so junk in a data chunk's
# own CRLF stays a framing error.
def route_chunktrail_bogus(self):
self.protocol_version = "HTTP/1.1"
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Transfer-Encoding", "chunked")
self.send_header("Connection", "close")
self.end_headers()
if self.command == "HEAD":
return
try:
body = b"<html><body><p>CHUNKTRAIL-BOGUS</p></body></html>"
self.wfile.write(b"%X\r\n" % len(body) + body + b"X-Foo: bar\r\n")
self.wfile.write(b"0\r\n\r\n")
self.wfile.flush()
except OSError:
pass
self.close_connection = True
# Body complete, then EOF before the blank line closing the section. The
# terminating chunk already arrived, so the payload stands.
def route_chunktrail_eof(self):
self.protocol_version = "HTTP/1.1"
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Transfer-Encoding", "chunked")
self.send_header("Connection", "close")
self.end_headers()
if self.command == "HEAD":
return
try:
body = b"<html><body><p>CHUNKTRAIL-EOF</p></body></html>"
self.wfile.write(b"%X\r\n" % len(body) + body + b"\r\n")
self.wfile.write(b"0\r\nX-Checksum: deadbeef\r\n")
self.wfile.flush()
except OSError:
pass
self.close_connection = True
# The direct-to-disk path, where the body never sits in memory.
def route_chunktrail_file(self):
self.send_chunked_trailed(
b"CHUNKTRAIL-BIN\n" + b"\x41\x42\x43\xfd" * 4096,
[b"X-Checksum: 0badc0de", b"X-Done: 1"],
"application/octet-stream",
)
# Aborts the chunked body with an RST, so the read fails rather than seeing a
# clean EOF and the transfer is already in error before the framing check.
def route_chunktrunc_reset(self):
@@ -2337,6 +2444,14 @@ class Handler(SimpleHTTPRequestHandler):
"/chunktrunc/stay.html": route_chunktrunc_stay,
"/chunktrunc/hostile.html": route_chunktrunc_hostile,
"/chunktrunc/reset.bin": route_chunktrunc_reset,
"/chunktrail/index.html": route_chunktrail_index,
"/chunktrail/one.html": route_chunktrail_one,
"/chunktrail/many.html": route_chunktrail_many,
"/chunktrail/none.html": route_chunktrail_none,
"/chunktrail/huge.html": route_chunktrail_huge,
"/chunktrail/bogus.html": route_chunktrail_bogus,
"/chunktrail/eof.html": route_chunktrail_eof,
"/chunktrail/file.bin": route_chunktrail_file,
"/errpage/index.html": route_errpage_index,
"/errpage/good.html": route_errpage_good,
"/errpage/missing.html": route_errpage_missing,
@@ -2665,6 +2780,12 @@ def main():
class BacklogHTTPServer(ThreadingHTTPServer):
request_queue_size = 128
# Skip the getfqdn() reverse lookup stock server_bind() does for the
# unread server_name: 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]
httpd = BacklogHTTPServer((args.bind, 0), factory)
if args.tls:

15
tests/nobacktrace.c Normal file
View File

@@ -0,0 +1,15 @@
/* Forces the empty backtrace() of an unwind-tableless build, for
143_engine-backtrace-empty.test. */
#define _GNU_SOURCE
/* The tree builds with -fvisibility=hidden, which would hide the interposer. */
#define SHIM_EXPORT __attribute__((visibility("default")))
SHIM_EXPORT int backtrace(void **buffer, int size);
SHIM_EXPORT int backtrace(void **buffer, int size) {
(void) buffer;
(void) size;
return 0;
}

235
tests/stringoom.c Normal file
View File

@@ -0,0 +1,235 @@
/* ------------------------------------------------------------ */
/*
HTTrack Website Copier, Offline Browser for Windows and Unix
Copyright (C) 1998 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/>.
Please visit our Website: http://www.httrack.com
*/
/* Drives the String allocation-failure path through a realloc stub: asking a
real allocator for a size it should refuse is a guess about the machine, not
a test (#915). One case per run, named by argv[1]. */
#include <setjmp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int alloc_fails = 0;
static int oom_jumps = 0;
static int oom_calls = 0;
static size_t oom_size = 0;
static size_t last_request = 0;
static jmp_buf oom_jump;
static char *test_realloc(char *buff, size_t size) {
last_request = size;
if (alloc_fails) {
return NULL;
}
return (char *) realloc(buff, size);
}
/* Declared before the include: StringBuffN_ and StringSprintf_ expand
STRING_OOM inside the header itself. */
static void test_oom(size_t size);
#define STRING_REALLOC(BUFF, SIZE) test_realloc(BUFF, SIZE)
#define STRING_FREE(BUFF) free(BUFF)
#define STRING_OOM(SIZE) test_oom(SIZE)
#include "htsstrings.h"
/* Either returns to the caller, leaving the String observable, or runs the
shipped handler. */
static void test_oom(size_t size) {
oom_calls++;
oom_size = size;
if (oom_jumps) {
longjmp(oom_jump, 1);
}
StringOom_(size);
}
/* File scope, so longjmp cannot leave them indeterminate. */
static String room = STRING_EMPTY;
static const char *kept = NULL;
static size_t kept_capacity = 0;
/* The handler must run once, and be told the size that was actually asked of
the allocator: an under-allocation reports a size the String does not have.
Compared against the stub's own record rather than a literal, so the initial
capacity stays a policy the test does not pin. */
static int failure_reported(const char *name) {
if (oom_calls != 1) {
printf("%s: FAIL (handler ran %d times)\n", name, oom_calls);
return 0;
}
if (oom_size != last_request) {
printf("%s: FAIL (reported %u bytes, allocator was asked for %u)\n", name,
(unsigned) oom_size, (unsigned) last_request);
return 0;
}
return 1;
}
/* Control: with the stub allocating for real, nothing must reach the handler.
Without this, a stub stuck in failing mode would "prove" every case. */
static int grow_case(void) {
size_t cap, i;
StringRoomTotal(room, 100);
if (oom_calls != 0) {
printf("grow: FAIL (handler ran %d times)\n", oom_calls);
return 1;
}
cap = StringCapacity(room);
if (StringBuff(room) == NULL || cap < 100) {
printf("grow: FAIL (capacity %u)\n", (unsigned) cap);
return 1;
}
/* A capacity never written to is a number, not a buffer: fill it to the
last byte, so an allocation short of the announced capacity is a heap
overflow the sanitizer legs catch. */
for (i = 0; i + 1 < cap; i++) {
StringBuffRW(room)[i] = (char) ('a' + (i % 26));
}
StringBuffRW(room)[cap - 1] = '\0';
for (i = 0; i + 1 < cap; i++) {
if (StringBuff(room)[i] != (char) ('a' + (i % 26))) {
printf("grow: FAIL (byte %u of %u read back as 0x%02x)\n", (unsigned) i,
(unsigned) cap, (unsigned char) StringBuff(room)[i]);
return 1;
}
}
if (strlen(StringBuff(room)) != cap - 1) {
printf("grow: FAIL (%u bytes readable, capacity %u)\n",
(unsigned) strlen(StringBuff(room)), (unsigned) cap);
return 1;
}
StringFree(room);
printf("grow: OK\n");
return 0;
}
static int hook_case(void) {
alloc_fails = oom_jumps = 1;
if (setjmp(oom_jump) == 0) {
StringRoomTotal(room, 100);
printf("hook: FAIL (grew through a failing allocator)\n");
return 1;
}
if (!failure_reported("hook")) {
return 1;
}
if (StringBuff(room) != NULL || StringCapacity(room) != 0 ||
StringLength(room) != 0) {
printf("hook: FAIL (capacity %u, buffer %s)\n",
(unsigned) StringCapacity(room),
StringBuff(room) == NULL ? "null" : "set");
return 1;
}
printf("hook: OK\n");
return 0;
}
/* The one the old code got wrong: a failed realloc must not overwrite the live
buffer with NULL nor bump the capacity past what was allocated. */
static int keep_case(void) {
StringCopy(room, "abc");
kept = StringBuff(room);
kept_capacity = StringCapacity(room);
alloc_fails = oom_jumps = 1;
if (setjmp(oom_jump) == 0) {
StringRoomTotal(room, 1000);
printf("keep: FAIL (grew through a failing allocator)\n");
return 1;
}
if (!failure_reported("keep")) {
return 1;
}
if (StringBuff(room) != kept || StringCapacity(room) != kept_capacity ||
StringLength(room) != 3 || strcmp(StringBuff(room), "abc") != 0) {
printf("keep: FAIL (buffer %s, capacity %u was %u)\n",
StringBuff(room) == kept ? "kept" : "moved",
(unsigned) StringCapacity(room), (unsigned) kept_capacity);
return 1;
}
alloc_fails = 0;
StringFree(room);
printf("keep: OK\n");
return 0;
}
/* The header's own growers expand STRING_OOM as well, so drive each of them
into the same failure rather than only the macro they call. */
static int sprintf_case(void) {
alloc_fails = oom_jumps = 1;
if (setjmp(oom_jump) == 0) {
StringSprintf(room, "%s", "x");
printf("sprintf: FAIL (formatted through a failing allocator)\n");
return 1;
}
if (!failure_reported("sprintf") || StringBuff(room) != NULL) {
return 1;
}
printf("sprintf: OK\n");
return 0;
}
static int buffn_case(void) {
alloc_fails = oom_jumps = 1;
if (setjmp(oom_jump) == 0) {
(void) StringBuffN(room, 10);
printf("buffn: FAIL (reserved through a failing allocator)\n");
return 1;
}
if (!failure_reported("buffn") || StringBuff(room) != NULL) {
return 1;
}
printf("buffn: OK\n");
return 0;
}
/* Runs the shipped handler, which must print and abort. */
static int abort_case(void) {
alloc_fails = 1;
StringRoomTotal(room, 100);
printf("abort: NOT aborted\n");
return 1;
}
int main(int argc, char **argv) {
const char *const mode = argc > 1 ? argv[1] : "";
if (strcmp(mode, "grow") == 0) {
return grow_case();
} else if (strcmp(mode, "hook") == 0) {
return hook_case();
} else if (strcmp(mode, "keep") == 0) {
return keep_case();
} else if (strcmp(mode, "sprintf") == 0) {
return sprintf_case();
} else if (strcmp(mode, "buffn") == 0) {
return buffn_case();
} else if (strcmp(mode, "abort") == 0) {
return abort_case();
}
fprintf(stderr, "usage: %s grow|hook|keep|sprintf|buffn|abort\n", argv[0]);
return 2;
}

View File

@@ -214,3 +214,14 @@ TESTS += 139_local-query-charref.test
TESTS += 140_crash-handler.test
TESTS += 141_webhttrack-warc-options.test
TESTS += 142_webhttrack-content-type.test
TESTS += 143_engine-backtrace-empty.test
TESTS += 144_engine-datadir.test
TESTS += 145_webhttrack-datadir.test
TESTS += 146_bash-shell.test
TESTS += 147_local-proxytrack-webdav-overflow.test
TESTS += 149_local-chunked-trailer.test
TESTS += 148_engine-spoolname.test
TESTS += 151_bash-shell-validate.test
TESTS += 150_engine-strsprintf.test
TESTS += 152_engine-string-oom.test
TESTS += 153_local-proxytrack-quiet.test

Some files were not shown because too many files have changed in this diff Show More