16 Commits

Author SHA1 Message Date
Xavier Roche
9b270becb5 Merge remote-tracking branch 'origin/master' into fix-908-bash-shell-validate 2026-08-02 12:45:32 +02:00
Xavier Roche
b4263417c3 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>
2026-08-02 12:41:26 +02: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
eab2929637 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>
2026-08-02 12:20:44 +02: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
b1218d8aa7 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>
2026-08-02 11:55:26 +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
Xavier Roche
8f383f4caf The installed html symlink is absolute, so an install tree cannot be moved (#888)
* Make the installed html symlink relative

The install-data-hook linked share/httrack/html to an absolute $(htmldir), so
an installed tree only worked at the prefix it was configured for: the link
dangled under DESTDIR staging and in any relocated copy, and webhttrack then
failed its test -d "${DISTPATH}/html" check and exited. It now emits a
relative link when htmldir sits under datadir.

The hook also sat in html/Makefile.am while writing into $(datadir)/httrack,
the directory lang/Makefile.am declares and populates, and it hardcoded
$(prefix)/share instead of following datadir. Moving it to lang/ with
$(langrootdir) drops that cross-subdirectory install-order dependency. A
stale symlink was previously left in place, so an upgrade kept an absolute
one; it is now replaced, a real directory is left alone with a diagnostic,
and a new uninstall-hook removes what install created. A moved datadir still
defeats webhttrack's own data search for an unrelated reason (#887).

The smoke test asserts no installed symlink is absolute and that the link,
resolved inside a copy of the tree at another path, stays inside that copy
and lands on the served UI. It now runs on Linux as well as macOS, since
Linux is where the affected packaging is consumed.

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

* ci: match the Linux build job's package list

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

* tests: put the stub browser where webhttrack looks first

webhttrack searches its own SRCHPATH (starting at $BINWD, then /usr/local/bin,
/usr/share/bin, /usr/bin) and only appends $PATH after it, so shadowing
x-www-browser through PATH only worked on hosts that have no real one. The
GitHub Linux runners ship /usr/bin/x-www-browser as Edge, which won the search
and then aborted on its SUID sandbox, failing the new Linux job.

Write the stub to $prefix/bin instead, which is SRCHPATH[0] on every platform,
and remove it in the teardown.

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 14:52:28 +00:00
57 changed files with 1993 additions and 268 deletions

View File

@@ -143,7 +143,7 @@ jobs:
# so point configure at it; everything else is in the SDK or default paths.
macos:
name: build (macOS arm64, clang)
runs-on: macos-14
runs-on: macos-15
steps:
- uses: actions/checkout@v7
with:
@@ -191,7 +191,7 @@ jobs:
# temp prefix, then check webhttrack brings up htsserver and serves the UI.
webhttrack-macos:
name: webhttrack smoke (macOS arm64)
runs-on: macos-14
runs-on: macos-15
steps:
- uses: actions/checkout@v7
with:
@@ -217,6 +217,95 @@ jobs:
- name: Smoke-test webhttrack
run: bash tests/webhttrack-smoke.sh "$RUNNER_TEMP/inst"
# Same smoke on the platform that ships webhttrack as a package: the install layout
# it asserts (a relative data symlink, #885) is what Debian consumes, and the macOS
# job alone left that untested on Linux.
webhttrack-linux:
name: webhttrack smoke (Linux x86-64)
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
with:
submodules: recursive
- name: Install build dependencies
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential autoconf automake libtool autoconf-archive \
zlib1g-dev libssl-dev libbrotli-dev libzstd-dev
- name: Build and install into a temp prefix
run: |
set -euo pipefail
./bootstrap
./configure --prefix="$RUNNER_TEMP/inst"
make -j"$(nproc)"
make install
- 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
@@ -593,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,18 @@ 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.
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
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,68 @@ 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
if test ! -x "$BASH_SHELL"; then
hts_bash_why="not an executable 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 +110,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 +161,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 +424,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

@@ -40,10 +40,3 @@ EXTRA_DIST = $(HelpHtml_DATA) $(HelpHtmlimg_DATA) $(HelpHtmlimages_DATA) \
$(HelpHtmldiv_DATA) $(WebHtml_DATA) $(WebHtmlimages_DATA) \
$(WebPixmap_DATA) $(WebIcon16x16_DATA) $(WebIcon32x32_DATA) $(WebIcon48x48_DATA) \
$(VFolderEntry_DATA) $(MetaInfo_DATA)
install-data-hook:
if test ! -L $(DESTDIR)$(prefix)/share/httrack/html ; then \
( cd $(DESTDIR)$(prefix)/share/httrack \
&& $(LN_S) $(htmldir) html \
) \
fi

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

@@ -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

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>

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

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

@@ -8,4 +8,24 @@ langroot_DATA = ../lang.def ../lang.indexes
EXTRA_DIST = $(lang_DATA) $(langroot_DATA)
# webhttrack and htsserver reach the served UI through $(langrootdir)/html, so the
# link is relative: an absolute $(htmldir) dangles under DESTDIR or relocation (#885).
install-data-hook:
@rel='$(htmldir)'; \
case '$(htmldir)' in \
'$(datadir)'/*) rel="../$${rel#$(datadir)/}" ;; \
esac; \
link='$(DESTDIR)$(langrootdir)/html'; \
if test -L "$$link"; then rm -f "$$link" || exit 1; fi; \
if test -e "$$link"; then \
echo "$$link exists and is not a symlink, leaving it alone" >&2; \
else \
echo " $(LN_S) $$rel $$link"; \
$(LN_S) "$$rel" "$$link"; \
fi
uninstall-hook:
@link='$(DESTDIR)$(langrootdir)/html'; \
if test -L "$$link"; then rm -f "$$link"; fi
#dist-hook:

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,
@@ -3480,6 +3498,10 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
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]) ? 0 : -1;
memcpy(&r, &(back[i].r), sizeof(htsblk));
back[i].r.is_write = 0; // mémoire
@@ -3490,7 +3512,7 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
back[i].r.is_file = 0;
//
// ligne par ligne
retour_fread = http_xfread1(&(back[i].r), -1);
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 +3650,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 +3849,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 +3862,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

@@ -1954,7 +1954,7 @@ LLint http_xfread1(htsblk * r, int bufl) {
} else if (bufl == -2) { // force reserve
if (r->adr == NULL) {
r->adr = (char *) malloct(8192);
r->adr = (char *) malloct(HTS_LINE_BLOCK_SIZE);
r->size = 0;
return 0;
}
@@ -1969,11 +1969,11 @@ 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) {

View File

@@ -248,6 +248,10 @@ 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
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"
@@ -3314,6 +3315,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 +6567,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 +7631,96 @@ 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; i < sizeof(caps) / sizeof(caps[0]); i++) {
for (len = caps[i] - 3; 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);
if (err)
break;
}
}
/* 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);
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 +7775,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 +7858,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 +7907,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,8 @@ Please visit our Website: http://www.httrack.com
#define HTS_STRINGS_DEFSTATIC
/* System definitions. */
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
/* GCC extension */
@@ -165,6 +167,44 @@ 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. **/
#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

@@ -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,10 @@ 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 (thisIsDir) { /* drop the trailing '/' */
StringPopRight(itemUrl);
}
if (thisIsDir == isDir) {
size_t size = 0;
@@ -1037,12 +1036,13 @@ 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;
@@ -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

@@ -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

@@ -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 images/header_title_4.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("/images/header_title_4.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,141 @@
#!/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)
trap 'set +e; rm -rf "$tmp"' EXIT
trap 'set +e; rm -rf "$tmp"; 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"
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
(cd "$rundir" && env "$@" bash "$tmp/src/configure" --disable-https) \
>"$rundir/log" 2>&1 || 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
reject missing 'is not an executable file' "BASH_SHELL=$tmp/missing/bash"
reject non-executable 'is not an executable file' "BASH_SHELL=$tmp/notexec/bash"
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

@@ -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

@@ -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,22 @@ 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
# 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 +49,10 @@ 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)
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 +63,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;
}

View File

@@ -214,3 +214,12 @@ 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

View File

@@ -1,23 +1,81 @@
#!/bin/bash
# Smoke-test an installed webhttrack: launch it with a stub browser and assert
# htsserver comes up and serves the web UI. Arg: the install prefix.
# htsserver comes up and serves the web UI. Args: the install prefix, and optionally
# the launcher to run instead of $prefix/bin/webhttrack (the macOS .app stub).
set -euo pipefail
prefix="${1:?usage: webhttrack-smoke.sh <install-prefix>}"
wht="$prefix/bin/webhttrack"
prefix="${1:?usage: webhttrack-smoke.sh <install-prefix> [launcher]}"
wht="${2:-$prefix/bin/webhttrack}"
test -x "$wht" || {
echo "no webhttrack at $wht" >&2
exit 1
}
browserstub="$prefix/bin/x-www-browser"
work="$(mktemp -d)"
# webhttrack backgrounds htsserver, which outlives it; reap any stray one (scoped
# to this prefix) so a lingering server can never hold the CI step open.
trap 'set +e; pkill -f "$prefix/bin/htsserver" 2>/dev/null || true; rm -rf "$work"' EXIT
trap 'set +e; pkill -f "$prefix/bin/htsserver" 2>/dev/null || true; rm -rf "$work" "$browserstub"' EXIT
export HOME="$work/home"
mkdir -p "$HOME/websites"
marker="$work/marker"
# No installed symlink may be absolute, or the tree only works at the prefix it was
# built for (#885). BSD find has no -lname, so read each link back by hand.
abs=""
nlink=0
while IFS= read -r l; do
nlink=$((nlink + 1))
case "$(readlink "$l")" in
/*) abs="$abs $l" ;;
esac
done < <(find "$prefix" -type l)
test -z "$abs" || {
echo "absolute symlink(s) in install tree:$abs" >&2
exit 1
}
# find's status is lost through the pipe, so an empty scan would pass vacuously.
test "$nlink" -gt 0 || {
echo "scanned no symlinks at all under $prefix" >&2
exit 1
}
# Locate the data dir the way webhttrack does, by the file it keys on, rather than
# assuming $prefix/share: --datadir moves it, and the link has to follow the data.
langdef=$(find "$prefix" -name lang.def -type f | head -1)
test -n "$langdef" || {
echo "no lang.def under $prefix" >&2
exit 1
}
htmllink="$(dirname "$langdef")/html"
test -L "$htmllink" || {
echo "no data symlink beside $langdef" >&2
exit 1
}
# Relocation: resolve the link inside a copy at another path and require it to stay
# inside that copy. An absolute link resolves back to $prefix and fails here.
reloc="$work/reloc"
cp -R "$prefix" "$reloc"
relocreal=$(cd "$reloc" && pwd -P)
htmlreal=$(cd "$reloc${htmllink#"$prefix"}" 2>/dev/null && pwd -P) || {
echo "relocated tree: the data link does not resolve" >&2
exit 1
}
case "$htmlreal" in
"$relocreal"/*) ;;
*)
echo "relocated tree: link escapes to $htmlreal" >&2
exit 1
;;
esac
# Resolving is not enough: it must land on the served UI, not just any directory.
test -d "$htmlreal/server" || {
echo "relocated tree: $htmlreal has no server/" >&2
exit 1
}
echo "install tree is relocatable"
stubdir="$work/bin"
mkdir -p "$stubdir"
@@ -34,14 +92,15 @@ exec /usr/bin/uname "$@"
EOF
chmod +x "$stubdir/uname"
# Stub browser: webhttrack tries its browser-name list in order and runs the
# first it finds, so shadow the first entry, "x-www-browser". It fetches the
# server URL and records PASS only for the working UI: the brand string, the
# step-2 form action, and an option-page tooltip, which a truncated/degraded
# template page would lack. htsserver only lives until webhttrack exits, so the
# check has to happen here.
# Stub browser, named after the first entry of webhttrack's browser list. It goes in
# $prefix/bin because webhttrack searches its own SRCHPATH before $PATH, so a real
# /usr/bin/x-www-browser (Edge, on the GitHub Linux runners) would beat a PATH shadow.
# It fetches the server URL and records PASS only for the working UI: the brand
# string, the step-2 form action, and an option-page tooltip, which a
# truncated/degraded template page would lack. htsserver only lives until webhttrack
# exits, so the check has to happen here.
# -a: the UI is served ISO-8859-1, so grep must not treat it as binary.
cat >"$stubdir/x-www-browser" <<EOF
cat >"$browserstub" <<EOF
#!/bin/bash
echo "stub browser invoked with: \$1" >&2
# Also fetch an option page and require a rendered title='' tooltip: proves the
@@ -65,8 +124,10 @@ else
echo "FAIL: unexpected response from \$1" >"$marker"
fi
EOF
chmod +x "$stubdir/x-www-browser"
export PATH="$stubdir:$prefix/bin:$PATH"
chmod +x "$browserstub"
# Deliberately NOT $prefix/bin: the launcher must find its payload from $0, and
# the browser stub is picked up through webhttrack's SRCHPATH, not $PATH.
export PATH="$stubdir:$PATH"
echo "launching webhttrack"
"$wht" </dev/null >"$work/webhttrack.log" 2>&1 &

33
tools/Info.plist.in Normal file
View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>HTTrack</string>
<key>CFBundleIdentifier</key>
<string>com.httrack.WebHTTrack</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>WebHTTrack</string>
<key>CFBundleDisplayName</key>
<string>WebHTTrack Website Copier</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>@PACKAGE_VERSION@</string>
<key>CFBundleVersion</key>
<string>@PACKAGE_VERSION@</string>
<key>LSMinimumSystemVersion</key>
<string>11.0</string>
<!-- Not background-only: the UI opens in the user's default browser. -->
<key>LSBackgroundOnly</key>
<false/>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>GNU General Public License v3</string>
</dict>
</plist>

123
tools/macos-app.sh Executable file
View File

@@ -0,0 +1,123 @@
#!/bin/sh
# Assemble HTTrack.app from an installed prefix, then verify it. POSIX sh on purpose: a
# bashism then fails shellcheck, not CI. Payload in Contents/Resources, where webhttrack
# resolves htsserver and its data relative to its own path.
set -eu
usage() {
echo "usage: $0 --prefix DIR --plist FILE [--out DIR]" >&2
exit 2
}
prefix=""
plist=""
out="."
while [ $# -gt 0 ]; do
case "$1" in
--prefix)
prefix="${2:?}"
shift 2
;;
--plist)
plist="${2:?}"
shift 2
;;
--out)
out="${2:?}"
shift 2
;;
*) usage ;;
esac
done
test -n "$prefix" || usage
test -n "$plist" || usage
test -r "$prefix/bin/webhttrack" || {
echo "no webhttrack under $prefix -- run make install first" >&2
exit 1
}
test -r "$plist" || {
echo "no Info.plist at $plist -- it is generated by configure" >&2
exit 1
}
prefix=$(cd "$prefix" && pwd -P)
list=$(mktemp)
trap 'rm -f "$list"' EXIT
app="$out/HTTrack.app"
rm -rf "$app"
mkdir -p "$app/Contents/MacOS" "$app/Contents/Resources"
cp "$plist" "$app/Contents/Info.plist"
cp -R "$prefix"/. "$app/Contents/Resources/"
rm -rf "$app/Contents/Resources/include"
find "$app/Contents/Resources" \( -name '*.la' -o -name '*.a' \) -delete
# webhttrack carries the configure-time datadir as a --datadir fallback (#887);
# in a bundle that is only a build-machine path, and the relative entries ahead
# of it already find the payload.
wht="$app/Contents/Resources/bin/webhttrack"
sed -e "s| \"$prefix/[^\"]*\"||g" "$wht" >"$wht.tmp"
mv -f "$wht.tmp" "$wht"
chmod +x "$wht"
cat >"$app/Contents/MacOS/HTTrack" <<'EOF'
#!/bin/sh
exec "$(dirname "$0")/../Resources/bin/webhttrack" "$@"
EOF
chmod +x "$app/Contents/MacOS/HTTrack"
fail() {
echo "bundle check: $*" >&2
exit 1
}
appreal=$(cd "$app" && pwd -P)
find "$app" -type l >"$list"
nlink=0
while IFS= read -r l; do
nlink=$((nlink + 1))
case "$(readlink "$l")" in
/*) fail "absolute symlink $l" ;;
esac
done <"$list"
test "$nlink" -gt 0 || fail "scanned no symlinks at all, the check proved nothing"
test -d "$app/Contents/Resources/share/httrack/html/server" ||
fail "Contents/Resources/share/httrack/html/server missing"
# Only our own library must be absent: system and Homebrew dylibs resolve from the
# user's own install.
if command -v otool >/dev/null 2>&1; then
find "$app/Contents" -type f -perm -u+x >"$list"
while IFS= read -r bin; do
file "$bin" | grep -q Mach-O || continue
if otool -L "$bin" | tail -n +2 | grep -q "$prefix"; then
otool -L "$bin" >&2
fail "$bin still links against the staging prefix (build with --disable-shared)"
fi
done <"$list"
fi
# otool sees load commands only, so scan the text payload separately. The binaries
# themselves still carry $(datadir) compiled in (#894), so they are not in scope here.
find "$app/Contents" -type f >"$list"
while IFS= read -r f; do
file "$f" | grep -q text || continue
grep -q -- "$prefix" "$f" 2>/dev/null && fail "$f embeds the staging prefix"
done <"$list"
# The plist version is a separate spot from the engine's, so drift is silent unless
# something compares them (#884 is what that looks like when nobody does).
plistver=$(awk '/<key>CFBundleShortVersionString<\/key>/ {
getline; gsub(/^[^>]*>|<[^<]*$/, ""); print; exit }' "$app/Contents/Info.plist")
binver=$("$app/Contents/Resources/bin/httrack" --version 2>/dev/null |
sed -n 's/.*[Vv]ersion \([0-9][0-9.]*[-.][0-9][0-9]*\).*/\1/p' | head -1 | tr '-' '.')
test -n "$binver" || fail "could not read a version out of the installed httrack"
test "$plistver" = "$binver" ||
fail "CFBundleShortVersionString says $plistver, httrack says $binver"
bundlever=$(awk '/<key>CFBundleVersion<\/key>/ {
getline; gsub(/^[^>]*>|<[^<]*$/, ""); print; exit }' "$app/Contents/Info.plist")
test "$bundlever" = "$binver" ||
fail "CFBundleVersion says $bundlever, httrack says $binver"
echo "built $appreal (version $plistver)"