Compare commits

...

34 Commits

Author SHA1 Message Date
Xavier Roche
77d03052a6 Release 3.49.20
Two engine fixes are the substance of this one. An off-site link whose name looked like a file was parsed as a page, and then spent the referring page's whole depth budget on the foreign site. And a mirror that was asked to quit while an FTP worker was still receiving printed its exit notice, then waited forever on that slot. The rest of the range is the documentation refresh, the WebHTTrack language menu, and test and CI work.

VERSION_INFO goes to 3:13:0, revision only: no installed struct changed shape, no export came or went. debian/changelog picks up the League Spartan webfont the docs now bundle, and Standards-Version 4.7.4 still matches current policy.
2026-08-10 21:11:46 +00:00
Xavier Roche
628fc70727 The Windows suite runs none of the tests covering the libraries it depends on (#1131)
* The Windows suite runs none of the tests covering the libraries it depends on

ci-windows-suite.sh selects its tests by topic word, and no glob matches the
harness self-tests: 254, 256 and 257 (testlib.sh) and 258 (crawllib.sh) never
run there, though every test the job does run now sources one of those
libraries. Two categories pull them in; 260 keeps its name clear of both until
#1126 explains why a deliberately failing crawl wedges the Win32 leg.

172 stubs one test per category, so it gains two and its tally moves with them.

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

* 258's httrack shim never reaches PATH on Windows, and 172 cannot see 260 come back

Two gaps the review found in the tests this PR newly selects there.

The argv shim in 258 lives under TMPDIR, which the Windows job sets to a
cygpath -m drive-letter path, so bash reads "D:/a/_temp/..." on PATH as two
entries and the real engine answers every case. testlib gains posixpath(),
nativepath's inverse, which 226 was already open-coding; 258 also proves the
shim is reachable rather than assuming it, since an unreachable one silently
tests the wrong binary.

172 could not kill a crawllib glob widened to *_crawl-*, which drops 258 and
selects 260: in the stub tree the tally happened to hold. A decoy named the way
260 is, selected by nothing, plus a duplicate-selection check on the progress
log, close both.

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

* The decoy and duplicate checks report after the tally that hides them

Every wrong selection also moves ran=, so the tally fired first and the two
named checks never got to say what went wrong. Reordered ahead of it, and the
decoy failure drops the whole suite output it was quoting.

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

* 256 stops at the first signal that breaks, so three of the four go unmeasured

Windows reds on the SIGINT leg: the subject sits out the whole sleep and exits 0,
so the trap never ran. The loop ends there and says nothing about HUP or QUIT,
which is the half needed to tell a missing trap from a signal MSYS will not
deliver to a bash of its own. It now reports every leg with the time each took.

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

* MSYS never delivers INT or QUIT to a bash its own kill sent them to

Measured on both Windows runners now that 256 reports every leg: the SIGINT and
SIGQUIT subjects sit out the whole 30s sleep and exit 0, where TERM and HUP
return at once with the trap's status. Never delivered, rather than delivered
and ignored, which is why 243 and 255 already skip there. Those two legs are cut
on Windows; the other four sections of 256 keep running.

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

* The 260 exclusion lives in a filename, where a rename would ship the wedge

Review found the #1126 exclusion resting on prose and a glob miss: rename 260
into one of the topic words and the Win32 leg wedges again, with nothing to say
why. The driver now names the file and stops when it is gone, so a rename reds
in the first second rather than after 45 minutes on the runner. 172 drives that
by renaming the staged copy.

Also from the review: 256 tests PIPE, the fifth signal in the trap list it had
never sent, and takes its signal set from an array rather than a split string;
258 keeps $shim native and converts only the PATH entry; two comments that had
grown by accretion are split back to one point each, and 258's overstated the
shim guard, which buys a clearer failure rather than the only one.

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

* Sending SIGPIPE was scope creep, and four Linux legs disagreed with my box

The fifth signal in cleanup_push's trap had never been sent, so I added it. On
the runners the subject sat out its whole sleep and exited 0, where bash 5.2.37
here returns at once with the trap's status: a self-sent SIGPIPE reaches a
non-interactive bash differently depending on the build. Reverted to the four
signals this test has always sent, and the gap is filed rather than guessed at.

Refs #1136

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-10 20:36:19 +00:00
Xavier Roche
7ff60e9de7 Reshoot the WebHTTrack guide screenshots without the loopback address (#1135)
The seven screens that showed the crawled address or the mirror folder now
read www.example.com.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 20:23:48 +00:00
Xavier Roche
4588831dcd The doc screenshots advertise 127.0.0.1 and a random port (#1133)
The crawled address is on screen in the wizard panes and names the mirror
folder, so every guide screenshot showing it, and the software-centre image,
carried a loopback artifact.

--site-host and --site-port let the walk address its own server by name; the
workflow maps www.example.com and lowers ip_unprivileged_port_start so it can
answer on 80. A local run keeps the old behaviour.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 19:44:26 +00:00
Xavier Roche
adbc46a5b8 The software-centre screenshot advertises a loopback address (#1132)
The wizard pane it pointed at has one piece of user content, the URL box, and
the walk crawls a throwaway loopback server, so the hero image read
http://127.0.0.1:43925/. The scan-rules pane shows real capability and no
address. Reverting to a wizard pane wants the walk to crawl a named host.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 19:40:30 +00:00
Xavier Roche
249b96b975 Advertise WebHTTrack with a screenshot from this decade (#1130)
The metainfo pointed at a 300x238 JPEG of the UI in a Mozilla window on
Windows 98 chrome, unchanged since 3.30.1. It points at a guide asset now, so
future reshoots refresh the software-centre listing on their own.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 20:54:24 +02:00
Xavier Roche
d8402d7ad2 local-crawl.sh keeps its own copy of the server launch and the crawl backstops (#1123)
* local-crawl.sh keeps its own copy of the server launch and the crawl backstops

crawllib.sh was extracted from the tests that drive local-server.py themselves,
leaving local-crawl.sh -- the launcher the other 62 crawl tests go through --
with a second copy of the launch block and of the --max-time/watchdog pair. It
now sources the library: the server comes up through local_server_start, the two
backstop constants live in one place, and the four near-identical httrack
invocations collapse into run_pass/require_pass.

The engine is still backgrounded here rather than run through local_crawl, which
would put it in its own process group where the suite watchdog's stack dump
cannot reach it (105).

--tls earns its place in the library now that a caller wants it, and 258 covers
it by fetching over TLS rather than reading the scheme back off BASEURL.

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

* 14_local-https asserts nothing about the scheme it crawled

Its audit (5 files, no errors, the page is there) reads the same whether the
crawl was TLS or plain, so a harness that lost --tls would turn it into a
duplicate of the plain-http tests. The mutation matrix for local-crawl.sh's
--tls passthrough survived on exactly that.

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

* Trim the comments added by the crawllib adoption

Signed-off-by: Xavier Roche <roche@httrack.com>

* Nothing tests that local-crawl.sh fails loudly

The 62 tests behind it read nothing but its exit status, so a harness that
swallowed a failed crawl would report every one of them green. 260 covers the
three ways a run must end badly: a crawl the engine refuses, a later pass that
fails while the first pass's mirror stands, and an audit that does not hold.
The middle one is the case the others cannot see, since a missing mirror ends
the run at the host-root check whether or not the pass was required to succeed.

run_pass now returns the crawl's status instead of leaving it in a global, and
require_pass runs the pass itself, so a call site names its log and label once
and cannot check the previous pass's result.

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

* Keep the new harness test out of the Windows suite's topic globs

Named 260_local-*, it was selected by ci-windows-suite.sh, and the Win32 leg
then died at its step timeout twice on the same commit while x64 passed. It is
the first Windows-selected test that deliberately fails a crawl, and the suite
kept neither a log nor an artifact, so the cause is unknown; #1126 records what
was measured. The other harness self-tests (254, 256-258) sit outside those
globs too, and widening the Windows selection is its own change.

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

* Do not name the Windows driver in the new test's comment

226 greps every .test for that filename and demands the credential-scrub line
when it finds one, since sourcing the driver starts an interpreter that can post
statuses. A comment cannot be told from a source line by that grep, so the
mention alone reddened every leg. 226 skips on this host, so only CI saw it.

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 17:22:47 +00:00
Xavier Roche
b268caa220 The guide still explains the language dropdown that #1086 removed (#1125)
The web note told readers the menu opens blank and that its first entry means
leave the interface alone. That placeholder option is gone; the menu now shows
the language in force and picking another reloads the page in it.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 17:21:23 +00:00
Xavier Roche
fb59dd6aba Reshoot the WebHTTrack screenshots after the masthead fix (#1129)
Every screen carries the masthead, so ef100363 dated all eighteen.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 17:18:19 +00:00
Xavier Roche
ef100363a7 The WebHTTrack tagline bar sits off the wordmark's baseline (#1127)
#927 and #1111 cancelled the SVG's built-in whitespace in html/doc.css, so the
doc pages tightened and the WebHTTrack UI, which renders the same masthead from
its own stylesheet, kept the gap.

Measured with getBoundingClientRect in the same browser: bar top minus image
bottom was 0 on the server pages against -1.09 on a doc page, and is now -1.09
on both.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 16:28:08 +00:00
Xavier Roche
c3e1a202c1 The GUI guide's screenshots are stale, and the palette step greys out their backgrounds (#1124)
* Reshoot the WebHTTrack screenshots in the GUI guide

The set was captured at 3.49-14, before the masthead wordmark and the rings
background became SVGs (#916, #910) and before the welcome page learned to
mark the language in force (#1086).

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

* Quantize the guide images by median cut, not max coverage

Max coverage spreads the 256 entries over the colour cube regardless of how
many pixels each region costs, so the large flat panel and rings backgrounds
came out grey or tinted. Mean channel error drops from 5.3 to 0.5, for 650K.

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-10 15:38:05 +00:00
Xavier Roche
f55f68c94e The documentation pages ask for Trebuchet before the system font, and have no display face (#1122)
doc.css put "Trebuchet MS" and Verdana ahead of system-ui in the body stack, so only Linux ever reached the platform font: Windows and macOS both stopped at Trebuchet. This reorders to system-ui first, keeps Trebuchet and Verdana behind it, and puts League Spartan 700 on h1 and h2. It goes on headings alone because it is a geometric display face, and it reads worse than the system font over the long prose of the reference pages. The website's stylesheet now does the same.

The font is self-hosted rather than linked from Google Fonts, since the pages are read from an installed directory and over file:// as often as over http. It is the latin subset at weight 700, 12888 bytes, with its OFL beside it, and `doc.css` reaches it by a relative URL that CSS resolves against the stylesheet rather than the document. New test 259 walks every `url()` in `html/*.css` and fails when the target is missing from the installed tree or the tarball, which test 225 cannot do: it drops the htmldir region on purpose, so either install rule could have gone missing with nothing going red.

`debian/copyright` gains an OFL-1.1 stanza, text inlined because OFL-1.1 is not in common-licenses. It previously declared every file GPL-3+, which is wrong for a bundled font. Debian ships `fonts-league-spartan`, but a `@font-face` cannot portably reach a fontconfig family and the tarball and macOS builds have no such package, so the bundled copy stays. Upstream declares no Reserved Font Name, which is what lets the subset keep the family name.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 16:10:25 +02:00
Xavier Roche
749de004c7 Engine self-tests assert a substring, so the value underneath can change unseen (#1121)
* tests: pin the engine self-test assertions that only matched a substring

Five self-tests asserted their result with a substring grep, so a change
in the value underneath went unseen: moving the advertised
Accept-Encoding from identity;q=0.9 to q=0.8 leaves "acceptencoding
self-test OK" in the output and the old test green. assert_selftest pins
the line and the exit status and prints want beside got.

Two more compared the whole line already but through a bare
test ... || exit 1, which reds without naming what differed.

The greps were also pipes into grep -q, which AGENTS.md forbids; at these
output sizes the producer finishes before grep exits, so removing them is
conformance rather than a fix.

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

* tests: shorten the watchdog capture comment

Two lines, and "can SIGPIPE" rather than a claim of a live bug: at the
param block's size sed always finishes writing, so the trap is latent.

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-10 11:18:35 +00:00
Xavier Roche
09e3e1f62b The tests driving the local server each rebuild its launch, and most crawl with no time cap (#1120)
* tests: give the local-server cluster a shared launch and crawl helper

Twenty-two tests drive tests/local-server.py themselves rather than
through local-crawl.sh, and each rebuilds the same launch: resolve the
script, find a python, background it, poll stdout for the ephemeral port,
register the reaping, build the base URL. Only three of their crawls
carried --max-time and none a watchdog, so a wedge here ran until CI
cancelled the job at 45 minutes, taking the log with it.

tests/crawllib.sh adds local_server_start and local_crawl, covered by
258_crawllib.test from a subject process.

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

* tests: review fixes on the local-server helpers

Drop local_crawl's --deadline, which no test outside its own used;
CRAWL_DEADLINE already drives the watchdog for local-crawl.sh. Truncate
the crawl log rather than append, matching every redirect it replaced.
Skip the reap when the server is already gone: 240 stops its own server
mid-test, and stop_server answers an unresolvable pid on Windows by
killing every python.exe.

235 keeps its own run_with_timeout: it already had a watchdog, and
local_crawl's failure would preempt the four messages it prints.

258 pins the whole argv per call and truncates it first, so an
assertion cannot pass on the previous crawl's arguments.

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

* tests: let a test disarm its server's teardown

The cleanup registered by local_server_start expands its arguments at
push time, so a test that stops its own server mid-run leaves the frame
holding a dead pid. Signalling a number the system has since recycled
kills an unrelated process, then stalls reap_bounded for its whole grace
period. Track the live servers in SRV_PIDS and register the slot, so
local_server_stop can empty it. 240 is the caller.

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-10 11:37:19 +02:00
Xavier Roche
4364e6c93e The engine self-test scripts each carry their own teardown, invocation and silent comparisons (#1118)
* tests: an assert_* vocabulary and an engine self-test façade

assert_eq/assert_match/assert_no_match/assert_file/assert_dir/assert_min_bytes
print what they wanted beside what arrived, and assert_selftest wraps the
httrack -O /dev/null -#test=NAME invocation the tier spells out by hand,
checking the exit status the old `test "$(...)" = ...` form discarded.
httrack_path resolves the binary the ten tests that cd away resolve themselves.

257 exercises each one from a subject process, since an assertion that fires
exits: every helper has a case that must hold and one that must fire.

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

* tests: the engine self-test tier joins the shared library

The 01_engine-* / 01_zlib-* tier sourced nothing: 50 hand-written trap pairs,
a repeated absolute-binary incantation, and comparisons ending in a bare
`|| exit 1`. Those become cleanup_push, httrack_path and the assert_* helpers.

Two latent traps go with them: the mime helper piped into head -1, which
SIGPIPEs the engine under pipefail, and half the signal traps only removed the
temp dir on EXIT.

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

* tests: review fixes on the assertion vocabulary

Drop assert_no_match, assert_dir and assert_min_bytes: 257 was their only
caller, and a dead helper is what PR #1113 deferred them to avoid. The
duplication they would serve (five copies of size_of, the grep -q <<< cluster)
lives in the tests PR 4 covers.

assert_file and assert_match gain real callers here instead: four artifact
checks and the renameover/cookieimport/direnum output matches.

The -O /dev/null comment claimed the engine refuses to start without an output
path. It does not: htscoremain.c dispatches -#test ahead of the usage gate.

hdr() in 01_engine-header.test kept its capture in an assignment, where errexit
can still see a failing engine, and httrack_path now names a failed cd rather
than returning a bare /httrack.

257 grows the cases that kill a prefix compare, a case-folding matcher, a
dropped label, and a status check satisfied by exit 127.

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

* tests: retry the proxytrack port pick instead of reding on a lost race

freeport hands out a port it has already closed, so between the pick and
proxytrack's own bind another process can take it: 79 lost that race on the
Windows x64 leg. Factor the pick, the listen wait and a three-attempt retry
into testlib.sh, and have the four proxytrack tests launch through it.

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

* tests: give each proxytrack attempt its own log, and fold the port pickers

Review of the retry found the .done marker the pty drainer writes by path
escapes the fresh inode, so a killed attempt could satisfy 153's wait_drained
and let its console-leak check read a half-copied log. Each attempt now writes
its own $ptlog. The two ports come from one freeport call, which cannot hand
back the same number twice, and the three copies of that bind-and-close trick
become one helper. A retry says so on stderr, a proxytrack that dies announcing
neither outcome is named as such, and a launcher that leaves no pid is caught.

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 <noreply@anthropic.com>
2026-08-10 07:59:28 +00:00
Xavier Roche
35319c05be SIGTERM never reaches teardown while an FTP worker is receiving (#1106)
`sig_finish` raises `state.exit_xh` on SIGTERM, but the mirror's wait loop in `hts_mirror_wait_for_next_file` never polled it: it leaves only when the link is done, when `back_checkmirror` fails, or when `hts_loop_tick` says so. Nothing there ends a live FTP worker, and the call that does, `ftp_stop_workers()` from `back_delete_all` and `hts_free_opt`, sits past the loop in teardown. So "Exit requested to engine" printed and the crawl then waited forever on the receiving slot. Polling `exit_xh` there and returning the way the two neighbouring abort checks already do lets the outer loop's own `exit_xh` test end the crawl and reach teardown. The poll fires on any nonzero value, so a fatal `exit_xh = -1` raised while a slot is live no longer wedges either.

`hts_mirror_process_user_interaction` needed the same poll. The loop above is only reached while a socket is free, so with every slot busy the mirror parks one frame higher, in the wait for a free socket, whose guards were the identical pair. Both polls are load-bearing: with only the inner one, three stalled links against two sockets still hang; with only the outer one, the single-link case does.

`^C` is untouched. It sets `state.stop`, not `exit_xh`, and neither poll reads it, so a first `^C` still finishes pending transfers and a second still exits immediately. That half of #1096 is a separate bug on a separate path, `wait_socket_receive` in `htsftp.c` never reading `state.stop`, and it is not fixed here, so the issue stays open. Nothing in the code makes letting a receiving slot finish a deliberate policy, and a stalled HTTP body does not behave like the FTP one: measured on master, SIGINT ends a stalled HTTP transfer in 0.1s and a stalled FTP one only after `--timeout` expires. The HTTP promptness is itself accidental, #1110.

One consequence worth naming. The `exit_xh` route falls through the whole completion sequence, `--purge-old` included, so a SIGTERM during an `--update` run can purge files it never re-fetched. That is pre-existing on that route, but this fix reaches it where the engine used to hang, and a user who reached for SIGKILL kept their mirror. Filed as #1109.

`tests/255_local-ftp-sigterm.test` signals a crawl parked on a stalled FTP body with `--timeout=0 --max-time=0`, so only the fix can end it. It pins the exit path rather than just the exit: reverting the hunk hangs, and returning the caller's error code or the already-done code leaves the wait too but loses the cache or drops the link silently, which the log assertions catch. A saturated leg covers the outer poll, and controls check that a single `^C` still completes an in-flight transfer, that the `^C` actually reached `sig_leave`, and that an unsignalled crawl is unaffected. It skips on Windows, where MSYS cannot deliver the signal to a native exe.

Refs #1096

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 07:40:12 +00:00
Xavier Roche
e4fbe856e5 The chrome check cannot see the masthead on two of the fifteen pages (#1116)
guide.html and index.html build their own sidebar, so they carried no markers at
all and the masthead above it drifted freely: changing the tagline in guide.html
left --check reporting 0 out of sync, where the same edit in faq.html was caught.

The masthead is byte-identical on all fifteen, so it becomes its own region that
a page can take without the generated navigation. Both now follow the tool: a
canary in MASTHEAD reaches 15 pages, where before it reached 13.

Closes #1115

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 08:19:41 +02:00
Xavier Roche
4f49eec86b The docs and the site disagree on what HTTrack is called (#1114)
The site says "Free software offline browser", these pages said "Open Source
offline browser", and the French tagline has always been "Aspirateur de sites
web libre", so one language already made the claim the other did not. Xavier
picked the site's wording for the web surfaces.

The lang/ catalogs keep theirs: they are the GUI, the English value is their
join key, and 17 carry a real translation that changing it would orphan.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 07:41:45 +02:00
Xavier Roche
f8ed3e771f Every test script carries its own copy of fail(), skip() and the teardown traps (#1113)
* tests: split the wedge forensics out of testlib.sh into proclib.sh

testlib.sh is sourced by 74 tests, but 235 of its lines are the process
forensics only the timeout driver and the Windows suite ever call.

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

* tests: fold the copied fail(), skip() and teardown traps into testlib.sh

62 copies of fail(), 7 of skip() and ~110 hand-written trap pairs go away.
cleanup_push installs the signal half two thirds of them never had, with
errexit off (#773); it takes a command and its arguments, not a shell
snippet, so it needs no eval.

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

* tests: fix the review findings on cleanup_push and the proclib split

Re-sourcing testlib.sh reset the cleanup stack, so 172 leaked its temp tree;
the stack is now self-preserving and 256 covers it. Both argv slices carry
the [@]+ guard an empty frame needs on bash 3.2. The engine matchers move to
proclib.sh beside their only consumers, dropping the SC2034 pair that hid the
cross-file reference. 100 and 105 register incrementally instead of rebuilding
an EXIT trap, which is what the LIFO stack is for; 103's scanner now inspects
the libraries, whose traps its errexit gate had stopped reaching. 250's dead
poll stub waits for the child to announce, closing the other half of #1092.

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

* tests: keep the default $top_srcdir relative

100_local-purge-longpath.test hands it straight to python.exe, which cannot
resolve the absolute MSYS path the library had started defaulting to, so the
fixture server never came up and the test failed where it used to skip. The
Windows suite exports no value of its own, so the default is what it runs on.

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-09 21:58:42 +02:00
Xavier Roche
4231a141d3 The documentation footer fails AA contrast in light mode (#1112)
#878 gave the footer color: #eef, which is 3.56:1 on the --field it sits on,
below the 4.5:1 AA wants for text this size. Dropping it inherits body's --ink
at 4.62:1; no lighter colour clears the bar on that field, and --ink-soft is
worse there at 2.38:1. The dark override already passes at 7.76:1.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 18:13:54 +02:00
Xavier Roche
314ee45350 The tagline bar still sits off the wordmark's baseline (#1111)
#927 gave the wordmark negative margins to put the black bar back against it,
but left the .4rem that pushes the bar down, so about 5.3px of gap survived the
fix meant to close it. Dropping it leaves the collapsed sibling margin at the
-1.1px #927 measured for the SVG's overshoot below the baseline.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 18:07:45 +02:00
Xavier Roche
54d93200a4 Two suite tests fail on the hppa, powerpc and ppc64 Debian buildds (#1108)
* Fix two suite tests that fail on the hppa, powerpc and ppc64 buildds

246_engine-connect-stop needs its connects to TEST-NET-1 still in flight, but
probed that the instant connect() returned; a network that rejects 192.0.2.1
rather than dropping it answers milliseconds later, and back_wait() then ends
slots the control asserts it left alone. Settle before re-reading the sockets.

183_altstack-worker pinned the worker's fault to SIGSEGV, which hppa does not
report for a spent thread stack. Accept any fault the handler covers, and print
the report when it is none of them.

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

* Tighten the two comments this change adds

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

* Match the worker's fault by signal name, not number

SIGBUS is 7 on x86 and 10 on hppa, so a numeric set cannot be right on
both.

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

* Gate the dead-host fixtures on a network that refuses TEST-NET-1

tools/hostile-net.sh runs a command in a netns whose peer owns 192.0.2.1 and
listens on nothing, behind 30ms of netem so the RST lands after connect()
returns rather than before it. That is the powerpc and ppc64 buildds, and
without the settle probe 246 fails under it with the buildds' exact log.

The script refuses to run when the address is dropped or answered, so a leg
that reproduces neither cannot pass silently. Widen SHELL_SCRIPTS to tools/*.sh
while here: it was an explicit list that had already lost mk-sbuild-chroot.sh.

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

* Cut the comments and rules this change adds

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

* Assert the hostile-network leg skipped on the settle probe

Without it a netem that stops delaying leaves the leg green while 246 skips on
the older check, so the branch the leg guards never runs.

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-09 16:59:11 +02:00
Xavier Roche
6e98e33495 The htsserver tests copy five helpers per file, and the copies have drifted (#1105)
* The htsserver tests copy five helpers per file, and the copies have drifted

Each of the ~20 tests driving htsserver carried its own freeport, start,
srvpid, portof and request, plus an inline python socket client: eleven
copies of freeport in three variants, eight of srvpid in six. Two of the
divergences were live bugs. Five copies read the announced pid with
"sed ... | head -1", which SIGPIPEs the producer so that under pipefail a
spurious 141 becomes the test's verdict; 91_webhttrack-directory.test wrote
a firstline() to dodge that and nobody copied it. And the socket timeout had
drifted to 15s in one copy and 30s in four, with only the 15s copy catching
socket.timeout and reporting, so the other four wedge where that one fails
cleanly.

tests/webhttracklib.sh and tests/httpclient.py now own picking the port, the
background launch under an ignored SIGTERM, the wait for the URL=
announcement, the pid the server reports (Windows announces none) and the
reaping of both. portof is gone entirely, since the library picks the port
and nothing has to parse it back out of the URL.

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

* Fix what the review found in the new htsserver test library

The --write-limit path backgrounded a pipeline, so $! was the reader rather
than the server. A start that never announced then killed only the reader,
orphaned the server on its port, and parked the reaping wait on the whole
job, which left the test immune to SIGTERM. Process substitution instead, so
the background pid is the server.

Centralising the launch also moved two python forks and a blocking wait
inside the one-second window 223 needs to mint two session ids in the same
second: under 8x load the window grew from ~0.28s to ~0.70s and the test
began skipping, which reads green while its assertion stops running.
htsserver_start takes --port and htsserver_freeport hands back several ports
bound together, so 223 picks both before the clock starts.

Also: htsserver_assert_reaped now covers the pid the server announces, and
refuses an empty list rather than passing on one; and firstline drops the
$'..' inside a parameter expansion that Apple's bash 3.2 cannot parse.

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

---------

Signed-off-by: Xavier Roche <xroche@gmail.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 16:55:17 +02:00
Xavier Roche
67c4668a3b A runner-image apt 403 reds a whole job, and the FTP active-mode branch never compiles (#1103)
Two CI gaps, in one PR because both land in `.github/workflows` and would collide.

`apt-get update` runs in 19 jobs under `set -euo pipefail`, so a 403 from `packages.microsoft.com`, a source the runner image ships and we never install from, takes a job down before a single package arrives. It reads like a real failure too: on #1093 the leg that died was named `msan (MemorySanitizer, clang)`. The update is now advisory in the 16 jobs that run on a GitHub runner, and `apt-get install` still fails loudly if a package we do need is missing. The three container jobs keep the strict form, since every source in a `ubuntu:devel` or `debian:sid` image is one we put there.

`FTP_PASV` is 1 in every build, so the `#if !FTP_PASV` active-mode branch is dead at runtime and never reaches a compiler either, which left #1087 re-verifying it by hand. The `build` job now recompiles `htsftp.c` alone with `-DFTP_PASV=0` and fails on any warning, which is why the define grew an `#ifndef` guard. It goes through make, so the object gets the flags the real one gets, and is dropped afterwards so `make check` links a normal library. The control is an undefined `accept` in that object: only the active branch calls it, so a probe that quietly rebuilt passive mode fails instead of passing. Two mutants kill the probe, a stray warning inside the branch and a revert of the `#ifndef`.

Closes #1091
Closes #1097


Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 13:42:18 +00:00
Xavier Roche
e6015e29bb Bump src/coucal so an in-tree build stops leaving the submodule dirty (#1102)
coucal's .gitignore now covers the libtool and automake droppings our
in-tree build drops into src/coucal (*.lo, *.la, .libs/, .deps/,
.dirstamp), same class as #905. Also picks up a codeql-action pin from
dependabot. No code change, no ABI impact.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 13:00:06 +00:00
Xavier Roche
ba53bca32e A foreign-host link that looks like a file is scanned as a page (#1093)
A link to `https://www.dropbox.com/s/xxx/distorted.jpg` looks like a file, so the engine accepts it even though it points off-site. Dropbox answers with `text/html`, and the preview page is then parsed with the referring page's whole depth budget. Every link on it is same-host from there, so one link mirrors the entire foreign site: 372 files in the reproduction against the one page it was entitled to, and wikipedia `File:` pages do the same. The new test drives two loopback servers and pins the mirror of the foreign one, which held 3 files under `-n` and 7 under `+*.jpg` before the fix.

At the parse gate such a link's depth now counts as 0, so the page is still fetched and stored, through the store-without-scan path a depth limit already uses, but nothing on it is followed. The gate is `is_html_mime_type()` rather than the wider hypertext set, or a foreign stylesheet would stop being parsed and its images would go missing; the one cost is a typeless response, which that macro reads as HTML and which now picks up a stray `.readme`. Hosts are compared through `jump_identification_const()`, since `adr` carries the scheme and any `user:pw@` and a raw compare made a site foreign to itself. Nothing about filters enters into it: the only exemption is being a seed, which the user asked for by name. Also fixes `hts_testlinksize`, which built its filter URLs with the separator on the wrong buffer.

Closes #121
Closes #1095


Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 12:45:23 +00:00
Xavier Roche
5b59cdb159 Run 230_local-ftp-userpass on Windows again (#1100)
The skip landed on a 3/3 wedge observed before #1039, #1055, #1071 and #1081 were in the tree. With all four merged, the test was re-run three times on current master with the gate and the `expected_skips` entry removed: six green legs, x64 and Win32, every run.

The runs were sequential on purpose. `windows-build`'s concurrency group is per-ref with `cancel-in-progress`, and overlapping Windows jobs are themselves a known way to lose a runner, so three parallel runs would have risked manufacturing the failure under test.

Detail and run IDs are on #1038.

Closes #1038

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 14:14:17 +02:00
Xavier Roche
c520d81f6b FTP active mode waits for the data connection with no timeout and no stop check (#1087)
Active mode waited for the server's data connection in a blocking accept(), so it ignored --timeout, --max-time and the stop flag, the way the two connects did before #1071. It now polls on the same 100ms tick, re-reading the stop flags and what is left of --max-time each pass, and puts the accepted socket back into blocking mode because BSD hands it the listener's non-blocking flag.

The branch is compiled out today (FTP_PASV is 1), so there is no runtime path to test. I compiled htsftp.c with FTP_PASV forced to 0 instead: clean, no warnings, and the object's undefined accept symbol confirms the branch was really built. The default is untouched. No build in CI covers that branch, which #1091 tracks, so this check was a one-off by hand.

A review found a second bug in the same code, folded in here: the active-mode listener and the data socket were both closed with a raw close() that left the descriptor in the variable, so the _CHECK_HALT_FTP after the transfer loop closed them again through _HALT_FTP. The data socket one is on the live PASV path. Both now reset to INVALID_SOCKET the way the PASV path already does around its own deletesoc calls. 253_local-ftp-close-once.test cancels a link mid-transfer through the engine's own per-link cancel, the one WebHTTrack's stop button pushes, and an LD_PRELOAD close() interposer reds the test on any EBADF. Reverting the data socket to its raw close gives exactly one.

Closes #1074
Closes #1090

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 14:14:13 +02:00
Xavier Roche
0748fa44e9 StringRoomTotal doubles its capacity without saturating (#1088)
StringRoomTotal doubled its capacity with no cap, so once past SIZE_MAX/2 the product wrapped to a capacity below the one already held and no further doubling climbed back out. Nothing reaches that today, since the realloc sits inside the loop and an outsized request dies in STRING_OOM several doublings earlier, but the tree had one grower that saturates (TypedArrayEnsureRoom, fixed in #1056) and one that did not. Past the halfway point it now asks for the requested total rather than doubling.

Clamping to SIZE_MAX, the obvious mirror of TypedArrayEnsureRoom, hands GCC a constant `realloc(p, SIZE_MAX)` and lights `-Walloc-size-larger-than` at every String growth site: 24 new warnings in proxytrack alone. The requested total is a runtime value, so there is nothing to fold. #1062 also asked for the realloct/freet wrappers; I left those alone, since they are plain aliases for realloc/free, while `freet` is a statement macro that assigns to its argument, htsstrings.h is installed, and pulling in htssafe.h leaks config.h into a header documented as standalone.

Two stringoom modes cover it. `saturate` forces the capacity past the halfway point, which nothing reaches through the API, and pins the size the allocator is asked for; it dies on its first allocation and so never sees a second step. `growloop` runs the whole loop against an allocator that keeps saying yes, under a step budget, so a clamp that fails to raise the capacity fails instead of spinning. Both go red on the old doubling and on a clamp that hands back the current capacity.

One unrelated fix rides along. `tests/250_timeout-poll-nofork.test` checked that the wedged child was gone with a single `kill -0` that nothing had waited for, and that race reddened this PR's own `build (no python3, Debian buildd)` leg. It now waits for the pid, bounded well under the child's own self-limit so a child expiring on its budget cannot pass for a successful kill.

Closes #1062
Closes #1092

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 14:14:01 +02:00
Xavier Roche
0ced83ccfc The heartbeat's sleep can inherit the caller's stdout, and no test sees it (#1089)
The `>/dev/null 2>&1` on `ci_suite_heartbeat`'s `sleep` is load-bearing, not noise suppression. The suite's EXIT trap kills the heartbeat shell and not the sleep it forked, so an orphan still holding the step's stdout keeps its reader waiting for EOF, which delays or loses the step the watchdog was there to protect.

171 is blind to it: its `sleep` is a shell function on a virtual clock, so nothing is forked and no descriptor is inherited, and the test stays green with the redirection deleted. The new leg goes in 172, which already drives the driver on real sleeps. Start the heartbeat on a 15s tick with its stdout in a pipe, let it enter the first sleep, kill it the way the trap does, then time how long the reader waits for EOF: 0s today, 13s with the redirection removed. Two things keep a prompt EOF from passing for the wrong reason: a control leg runs first and forks a child that does keep the pipe, and the leg samples the heartbeat pid before it signals, since one that died before forking its sleep would look identical.

Costs 172 about 8 seconds.

Closes #949

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 14:13:57 +02:00
Xavier Roche
171ffb0754 Tests 13 and 52 mint a cert the tree already ships (#1101)
* Tests 13 and 52 mint a cert the tree already ships (#1099)

Both minted a two-day self-signed cert with openssl req on every run, which
is why both carried openssl in their skip gate. make_tls_pem now joins the
tracked tests/server.crt and tests/server.key, the pair local-crawl.sh
already hands every --tls crawl, so the gate and the keygen go away.

Closes #1099

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

* Assert the fixture against load_cert_chain, not against the helper

The two byte comparisons only restated what make_tls_pem cats: a
server.key that does not match server.crt passed 254 and reddened 13
and 52. Load the pem the way the python servers do, and trim the
comments to one line each.

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

* Name the write in the fixture helper's failure message

A missing destination directory fails the redirect, not the read, and
the old wording blamed the fixture for it.

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 10:42:24 +00:00
Xavier Roche
e12fe63246 A failing cert fixture leaves tests 13 and 52 with an empty log (#1098)
`13_crawl_proxy_https` and `52_local-socks5` both built their self-signed cert with an `openssl req` whose output went to `/dev/null 2>&1`. Under `set -e` a failure there exits 1 before either test's first `echo`. Automake's log then kept only the `FAIL ... (exit status: 1)` line, with no clue what failed. It happened twice on a macOS 26.5 box.

The fixture was identical in both tests, so it moves to `testlib.sh` as `make_tls_pem`, which keeps openssl's output and prints it when the command fails.

`254_testlib-tls-pem.test` checks the helper from both sides. A working run has to produce a pem holding the key and then the certificate, in the order the callers' `load_cert_chain()` reads. An `openssl` shimmed onto PATH that exits 3 has to make the helper exit 1 with that openssl's reason on stderr; the shim still writes the files it was asked for, so nothing but the helper's own exit can satisfy the status check. Each of those assertions was confirmed against a mutant that breaks it.

Leaving `openssl` out of the picture entirely looks better still: `tests/server.crt` and `tests/server.key` are already tracked and distributed, with a SAN and a 2056 expiry. Moving 13 and 52 onto them would drop `openssl` from their skip gates, but it changes where those two tests run, so it belongs in its own change.

Closes #1094


Signed-off-by: Xavier Roche <xroche@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 11:48:09 +02:00
Xavier Roche
ff266a0e61 The macOS Release asset name does not say it is arm64-only (#1085)
* The macOS Release asset name does not say it is arm64-only (#1083)

The DMG is arm64-only, but the file attached to the Release is
HTTrack-<version>.dmg, so nothing in the name warns an Intel user that it
will not launch. Everything else already says arm64: the job title, the
upload artifact, and the copy httrack.com serves.

The rename happens at the Release upload rather than at build time because
httrack-com's stage_macos_dmg.sh asserts the artifact basename is exactly
HTTrack-$VER.dmg, so renaming earlier would break staging in that repo.

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

* Shorten the rename comment

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

* Name the DMG at build time, not at the Release upload

Xavier's call: the image stays arm64-only and says so in its name. Naming it
where it is created gives the artifact, the Release asset and the logs one
name, instead of making the workflow a second authority over it.

The arch comes from the bundle's own Mach-O list, and counts only if every
one of them carries it, so a thin dylib cannot get the image called universal.

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

* Guard the arch intersection against an empty token

Review found that a trailing space in lipo's output would put an empty
token in the set, which every binary then "carries", so a thin arm64
build would be named universal. Apple's lipo emits none, checked with
od -c against a real bundle on an Apple Silicon machine, so this is
insurance rather than a live bug: without the guard that input yields
universal, with it arm64.

lipo joins the tool preflight too, so a missing one says so instead of
reporting that the binaries share no architecture.

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-09 07:31:27 +00:00
Xavier Roche
dfc0efff3a WebHTTrack's language menu never marks the language in force (#1086)
* WebHTTrack's language menu never marks the language in force (#1084)

The welcome page opened its menu blank instead of showing the active
language: index.html used ${list:#iso}, and that form carries no default.
The listid: form does, and the launcher already hands htsserver the index
as "lang".

Switching the template alone was not enough. Format 2 opens its first
option before the loop that applies the default, so index 1 (English, the
commonest case) could never be marked. That opening now checks the default
as well, which also covers the nine other listid: menus whose default is 1;
there the first option was already the one the browser displayed, so
nothing changes on screen.

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

* Drop the placeholder option and assert the rendered language

Review findings: the empty <option value=0> could itself post lang=0,
which the n > 0 guard ignores, leaving the menu blank again; and the test
asserted the menu alone, so it passed on a menu that merely echoed the
requested index while the body rendered another language.

The placeholder is gone, and test 251 now checks the page's own greeting
and covers the case where no language is set.

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

* Keep the test's server PIDs out of a subshell

serve() printed the URL, so callers ran it as url=$(serve ...) and the
pids it recorded died with that subshell. Cleanup then killed nothing and
the four servers outlived the test, which the macOS runner reports as
orphan processes and fails the job on.

It sets ${url} instead. Verified by count: the old shape leaves four
htsserver processes behind, this one leaves none.

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-09 07:14:27 +00:00
312 changed files with 4204 additions and 3319 deletions

View File

@@ -22,7 +22,7 @@ jobs:
- name: Install appstreamcli
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get update || true # a third-party source in the runner image can 403 (#1097)
sudo apt-get install -y --no-install-recommends appstream
appstreamcli --version

View File

@@ -38,7 +38,7 @@ jobs:
- name: Install build dependencies
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get update || true # a third-party source in the runner image can 403 (#1097)
sudo apt-get install -y --no-install-recommends \
build-essential clang autoconf automake libtool autoconf-archive \
zlib1g-dev libssl-dev libbrotli-dev libzstd-dev \
@@ -80,6 +80,27 @@ jobs:
- name: Build
run: make -j"$(nproc)"
# FTP_PASV is 1 in every build, so #if !FTP_PASV never reaches a compiler
# and rots unnoticed (#1091). Compiling is all this proves; nothing runs it.
- name: Compile the FTP active-mode branch
run: |
set -euo pipefail
objs="src/libhttrack_la-htsftp.lo src/libhttrack_la-htsftp.o src/.libs/libhttrack_la-htsftp.o"
# accept() marks active-mode code: absent from the object Build made,
# present below. One-sided, it passes on a file that ignored the define.
syms=$(nm --undefined-only src/.libs/libhttrack_la-htsftp.o)
if grep -qw accept <<<"$syms"; then
echo "::error::accept() is reachable with FTP_PASV=1; the marker no longer discriminates"
exit 1
fi
rm -f $objs
# Through CC: overriding CPPFLAGS would drop configure's
# -D_FORTIFY_SOURCE and the object-size warnings that come with it.
make -C src libhttrack_la-htsftp.lo CC="$CC -DFTP_PASV=0 -Werror"
syms=$(nm --undefined-only src/.libs/libhttrack_la-htsftp.o)
grep -qw accept <<<"$syms" || { echo "::error::-DFTP_PASV=0 built no active-mode code"; exit 1; }
rm -f $objs
# A backstop only: tests/test-timeout.sh bounds each test, and a healthy run
# is a minute here, two on macOS. Without it a stall ran to the job's 6h default.
- name: Test
@@ -108,7 +129,7 @@ jobs:
- name: Install build dependencies
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get update || true # a third-party source in the runner image can 403 (#1097)
sudo apt-get install -y --no-install-recommends \
build-essential autoconf automake libtool autoconf-archive \
zlib1g-dev libssl-dev libbrotli-dev libzstd-dev
@@ -138,6 +159,48 @@ jobs:
if: failure()
run: cat tests/test-suite.log 2>/dev/null || true
# Reproduces the buildds that refuse TEST-NET-1 instead of dropping it, where
# every fixture needing a stalled connect must skip rather than fail (#1108).
hostile-network:
name: build (network refuses TEST-NET-1)
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 iproute2
- name: Configure
run: |
set -euo pipefail
autoreconf -fi
./configure
- name: Build
run: make -j"$(nproc)"
- name: Test on a network that refuses TEST-NET-1
timeout-minutes: 20
run: |
set -euo pipefail
jobs=$(( $(nproc) * 2 )); [ "$jobs" -le 16 ] || jobs=16
tools/hostile-net.sh make check -j"$jobs"
# A refusal landing before connect() returns skips on the older probe
# instead, leaving the settle this leg exists to guard unrun.
cat tests/246_engine-connect-stop.log
grep -q 'within 500 ms' tests/246_engine-connect-stop.log
- name: Print the test log on failure
if: failure()
run: cat tests/test-suite.log 2>/dev/null || true
# The Ubuntu buildds run a development release every runner above is well behind.
# That gap is what shipped 3.49.18 broken on every Ubuntu architecture but
# riscv64, when 25.10 made uutils the default coreutils (#1042).
@@ -292,7 +355,7 @@ jobs:
- name: Install build dependencies
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get update || true # a third-party source in the runner image can 403 (#1097)
sudo apt-get install -y --no-install-recommends \
build-essential autoconf automake libtool autoconf-archive \
zlib1g-dev libssl-dev libbrotli-dev libzstd-dev
@@ -401,7 +464,7 @@ jobs:
run: |
set -euo pipefail
sudo dpkg --add-architecture i386
sudo apt-get update
sudo apt-get update || true # a third-party source in the runner image can 403 (#1097)
sudo apt-get install -y --no-install-recommends \
build-essential gcc-multilib autoconf automake libtool \
autoconf-archive zlib1g-dev:i386 libssl-dev:i386 \
@@ -444,7 +507,7 @@ jobs:
- name: Install build dependencies
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get update || true # a third-party source in the runner image can 403 (#1097)
sudo apt-get install -y --no-install-recommends \
build-essential autoconf automake libtool autoconf-archive \
zlib1g-dev libssl-dev libbrotli-dev libzstd-dev
@@ -499,7 +562,7 @@ jobs:
- name: Install build dependencies
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get update || true # a third-party source in the runner image can 403 (#1097)
sudo apt-get install -y --no-install-recommends \
build-essential clang autoconf automake libtool autoconf-archive \
zlib1g-dev
@@ -548,7 +611,7 @@ jobs:
- name: Install build dependencies
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get update || true # a third-party source in the runner image can 403 (#1097)
sudo apt-get install -y --no-install-recommends \
build-essential clang autoconf automake libtool autoconf-archive \
zlib1g-dev libssl-dev libbrotli-dev libzstd-dev
@@ -584,7 +647,7 @@ jobs:
- name: Install build dependencies
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get update || true # a third-party source in the runner image can 403 (#1097)
sudo apt-get install -y --no-install-recommends \
build-essential clang autoconf automake libtool autoconf-archive \
zlib1g-dev libssl-dev libbrotli-dev libzstd-dev
@@ -614,7 +677,7 @@ jobs:
- name: Install build dependencies (no libssl)
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get update || true # a third-party source in the runner image can 403 (#1097)
sudo apt-get install -y --no-install-recommends \
build-essential autoconf automake libtool autoconf-archive zlib1g-dev
@@ -651,7 +714,7 @@ jobs:
- name: Install build dependencies
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get update || true # a third-party source in the runner image can 403 (#1097)
sudo apt-get install -y --no-install-recommends \
build-essential autoconf automake libtool autoconf-archive \
zlib1g-dev libssl-dev libbrotli-dev libzstd-dev
@@ -730,7 +793,7 @@ jobs:
- name: Install build dependencies
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get update || true # a third-party source in the runner image can 403 (#1097)
sudo apt-get install -y --no-install-recommends \
build-essential autoconf automake libtool autoconf-archive \
zlib1g-dev libssl-dev libbrotli-dev libzstd-dev
@@ -790,17 +853,14 @@ jobs:
src/webhttrack.in
tests/*.sh
tests/*.test
tools/macos-app.sh
tools/macos-bundle.sh
tools/macos-release.sh
tools/mkdeb.sh
tools/*.sh
steps:
- uses: actions/checkout@v7
- name: Install linters
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get update || true # a third-party source in the runner image can 403 (#1097)
# noble ships shfmt 3.8.0 (universe), matching the pinned local dev
# version; use it rather than fetching a release binary from github.com.
sudo apt-get install -y --no-install-recommends shellcheck shfmt appstream \
@@ -859,7 +919,7 @@ jobs:
| sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc >/dev/null
echo "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-19 main" \
| sudo tee /etc/apt/sources.list.d/llvm-19.list >/dev/null
sudo apt-get update
sudo apt-get update || true # a third-party source in the runner image can 403 (#1097)
sudo apt-get install -y --no-install-recommends clang-format-19
# The clang-format-19 package ships the git-clang-format driver;
# expose it unsuffixed so "git clang-format" finds it.

View File

@@ -33,7 +33,7 @@ jobs:
- name: Install build dependencies
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get update || true # a third-party source in the runner image can 403 (#1097)
sudo apt-get install -y --no-install-recommends \
build-essential autoconf automake libtool autoconf-archive \
zlib1g-dev libssl-dev

View File

@@ -34,7 +34,7 @@ jobs:
- name: Install build dependencies
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get update || true # a third-party source in the runner image can 403 (#1097)
sudo apt-get install -y --no-install-recommends \
build-essential autoconf automake libtool autoconf-archive \
zlib1g-dev libssl-dev libbrotli-dev libzstd-dev
@@ -52,6 +52,15 @@ jobs:
pip install --quiet playwright
playwright install --with-deps chromium
# The address is on screen in the wizard panes and names the mirror folder,
# so the crawl needs a presentable host rather than 127.0.0.1 and a random
# port. example.com is reserved for documentation (RFC 2606).
- name: Let the crawl address a named site on port 80
run: |
set -euo pipefail
echo "127.0.0.1 www.example.com" | sudo tee -a /etc/hosts
sudo sysctl -w net.ipv4.ip_unprivileged_port_start=80 # bind 80 unprivileged
- name: Capture
env:
LANGN: ${{ inputs.lang || '1' }}
@@ -61,6 +70,7 @@ jobs:
# --root is the dist root; the source tree already holds every piece
# htsserver reads, so this needs no install.
python3 tools/screenshot-walk.py --htsserver src/htsserver --root . \
--site-host www.example.com --site-port 80 \
--lang "$LANGN" --scale "$SCALE" --out shots
- name: Upload the screens

View File

@@ -46,6 +46,12 @@ the operational checklist: toolchain, invariants, and how to ship a change.
platform-specific: GNU `tail | head -c 3` survives, the uutils coreutils leg
turns the same line into exit 141 with no output at all. Let the reader seek
instead: `od -An -c -j <skip> -N <len> file`.
- Never assert a fault by signal *number*: SIGBUS is 7 on x86, arm, powerpc and
s390x but 10 on hppa, alpha, mips and sparc. Map it with `kill -l "$n"`.
- A fixture that needs a host to stay silent must settle (500ms), re-probe every
socket, and SKIP when one answered: the powerpc and ppc64 buildds refuse
TEST-NET-1 milliseconds after `connect()` where runners drop it.
`tools/hostile-net.sh` runs a command on such a network.
## Hard invariants
- **Generated autotools files are NOT in git.** `configure`, every

View File

@@ -1,6 +1,6 @@
AC_PREREQ([2.71])
AC_INIT([httrack], [3.49.19], [roche+packaging@httrack.com], [httrack], [http://www.httrack.com/])
AC_INIT([httrack], [3.49.20], [roche+packaging@httrack.com], [httrack], [http://www.httrack.com/])
AC_COPYRIGHT([
HTTrack Website Copier, Offline Browser for Windows and Unix
Copyright (C) 1998-2015 Xavier Roche and other contributors
@@ -29,6 +29,8 @@ AC_CONFIG_SRCDIR(src/httrack.c)
AC_CONFIG_MACRO_DIR([m4])
AC_CONFIG_HEADERS(config.h)
AM_INIT_AUTOMAKE([subdir-objects])
# 3:13:0: revision-only bump. htsstrings.h's StringRoomTotal grew saturation in
# its macro body; no installed struct moved and no export came or went.
# 3:12:0: revision-only bump. httrackp gained a tail field (singlefile_state) and
# htsopt.h added volatile qualifiers; no layout moved, nothing went away.
# 3:11:0: revision-only bump. #1001 moved SOCaddr_inetntoa_ out of htsnet.h, adding
@@ -42,7 +44,7 @@ AM_INIT_AUTOMAKE([subdir-objects])
# moves nothing). Soname stays .so.3: HTTrackQt is the only consumer of the installed
# headers, so a libhttrack4 rename isn't worth it.
# (3:0:0 was the htsblk mime-buffer widening, the ABI break that moved .so.2 -> .so.3.)
VERSION_INFO="3:12:0"
VERSION_INFO="3:13:0"
AM_MAINTAINER_MODE
AC_USE_SYSTEM_EXTENSIONS

10
debian/changelog vendored
View File

@@ -1,3 +1,13 @@
httrack (3.49.20-1) unstable; urgency=medium
* New upstream release: an off-site link that looked like a file could pull
in the whole foreign site, and a mirror asked to stop while receiving over
FTP hung; full list in history.txt. The documentation was also refreshed.
* debian/copyright: record the League Spartan webfont the documentation now
bundles (OFL-1.1).
-- Xavier Roche <xavier@debian.org> Mon, 10 Aug 2026 21:08:40 +0200
httrack (3.49.19-1) unstable; urgency=medium
* New upstream release: mostly FTP fixes, plus stack-overflow, mirror

95
debian/copyright vendored
View File

@@ -44,6 +44,13 @@ License: FSFAP
permitted in any medium without royalty provided the copyright notice and
this notice are preserved. This file is offered as-is, without any warranty.
Files: html/fonts/*
Copyright: 2020 The League Spartan Project Authors
License: OFL-1.1
Comment:
The woff2 is the latin subset at weight 700, as served by Google Fonts.
Upstream declares no Reserved Font Name, so the subset keeps the family name.
Files: debian/*
Copyright: 2002-2026 Xavier Roche <xavier@debian.org>
License: GPL-3+
@@ -106,3 +113,91 @@ License: BSD-3-clause
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
License: OFL-1.1
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
.
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
.
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
.
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
.
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@@ -4,6 +4,19 @@ HTTrack Website Copier release history:
This file lists all changes and fixes that have been made for HTTrack
3.49-20
+ Fixed: a link to another host whose name looked like a file was scanned as a page, so one link could mirror the whole foreign site on the referring page's depth budget (#121)
+ Fixed: a size-bounded filter built both of its filter URLs wrong when the path had no leading slash (#1095)
+ Fixed: a mirror told to quit while an FTP file was being received announced the exit and then waited forever on that slot (#1106)
+ Fixed: stopping an FTP transfer closed its data socket twice, so a threaded crawl could lose another worker's connection (#1090)
+ Fixed: WebHTTrack's welcome page opened its language menu blank instead of marking the language in use (#1084)
+ Fixed: two suite tests failed on the hppa, powerpc and ppc64 Debian buildds, holding the package back on those ports (#1108)
+ Fixed: a growable string could wrap its capacity to zero instead of saturating (#1062)
+ Changed: the macOS disk image carries its architecture in its name, so an Intel user can tell before downloading that the build is for Apple Silicon (#1083)
+ Changed: refreshed the GUI guide, its screenshots, the documentation typography and the footer contrast
+ Changed: settled on one spelling of the product name across the docs and the site
+ Changed: multiple internal hardening, build, test and CI improvements
3.49-19
+ Fixed: an over-long user name in an ftp:// URL was truncated at the credential boundary, so the login went to a different account (#1032)
+ Fixed: an over-long FTP path or host name aborted the process instead of failing that one link (#1019)

View File

@@ -4,6 +4,7 @@ HelpHtmldir = $(htmldir)
HelpHtmlimgdir = $(HelpHtmldir)/img
HelpHtmldivdir = $(HelpHtmldir)/div
HelpHtmlimagesdir = $(HelpHtmldir)/images
HelpHtmlfontsdir = $(HelpHtmldir)/fonts
HelpHtmlTxtdir = $(HelpHtmldir)
WebHtmldir = $(HelpHtmldir)/server
WebHtmlimagesdir = $(HelpHtmldir)/server/images
@@ -27,6 +28,8 @@ HelpHtml_DATA = $(srcdir)/*.html $(srcdir)/*.js $(srcdir)/*.css
HelpHtmldiv_DATA = div/search.sh
HelpHtmlimg_DATA = $(srcdir)/img/*
HelpHtmlimages_DATA = $(srcdir)/images/*
# doc.css reaches the woff2 by a relative URL, so it installs beside the pages.
HelpHtmlfonts_DATA = $(srcdir)/fonts/*
HelpHtmlTxt_DATA = ../greetings.txt ../history.txt ../license.txt
WebHtml_DATA = $(srcdir)/server/*.html $(srcdir)/server/*.js $(srcdir)/server/*.css
WebHtmlimages_DATA = $(srcdir)/server/images/*
@@ -45,6 +48,7 @@ VFolderEntry_DATA = $(srcdir)/server/div/*.desktop
MetaInfo_DATA = $(srcdir)/server/div/*.metainfo.xml
EXTRA_DIST = $(HelpHtml_DATA) $(HelpHtmlimg_DATA) $(HelpHtmlimages_DATA) \
$(HelpHtmlfonts_DATA) \
$(HelpHtmldiv_DATA) $(WebHtml_DATA) $(WebHtmlimages_DATA) \
$(WebPixmap_DATA) $(WebIcon16x16_DATA) $(WebIcon32x32_DATA) $(WebIcon48x48_DATA) \
$(WebIcon64x64_DATA) $(WebIcon128x128_DATA) $(WebIcon256x256_DATA) \

View File

@@ -17,7 +17,7 @@
<header class="masthead">
<img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier">
<div class="tagline">Open Source offline browser</div>
<div class="tagline">Free software offline browser</div>
</header>
<div class="wrap">

View File

@@ -17,7 +17,7 @@
<header class="masthead">
<img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier">
<div class="tagline">Open Source offline browser</div>
<div class="tagline">Free software offline browser</div>
</header>
<div class="wrap">

View File

@@ -17,7 +17,7 @@
<header class="masthead">
<img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier">
<div class="tagline">Open Source offline browser</div>
<div class="tagline">Free software offline browser</div>
</header>
<div class="wrap">

View File

@@ -17,7 +17,7 @@
<header class="masthead">
<img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier">
<div class="tagline">Open Source offline browser</div>
<div class="tagline">Free software offline browser</div>
</header>
<div class="wrap">

View File

@@ -27,7 +27,7 @@
<header class="masthead">
<img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier">
<div class="tagline">Open Source offline browser</div>
<div class="tagline">Free software offline browser</div>
</header>
<div class="wrap">

View File

@@ -17,7 +17,7 @@
<header class="masthead">
<img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier">
<div class="tagline">Open Source offline browser</div>
<div class="tagline">Free software offline browser</div>
</header>
<div class="wrap">

View File

@@ -32,6 +32,20 @@
}
}
/* Additions stay out of the palette block above: it is kept byte-identical with the website's. */
:root {
--body-font: system-ui, -apple-system, "Segoe UI", Roboto, "Trebuchet MS", Verdana, sans-serif;
}
/* Relative URL: the docs are read from an installed directory too. */
@font-face {
font-family: "League Spartan";
src: url(fonts/league-spartan-700-latin.woff2) format("woff2");
font-weight: 700;
font-style: normal;
font-display: swap;
}
* { box-sizing: border-box; }
/* The plain declaration before each var() is what IE11 gets: it drops the
@@ -44,6 +58,7 @@ body {
color: #111;
color: var(--ink);
font: 1em/1.6 "Trebuchet MS", Verdana, system-ui, -apple-system, "Segoe UI", sans-serif;
font-family: var(--body-font);
}
/* ---- page frame ---- */
@@ -72,7 +87,6 @@ body {
/* A one-line label has no use for the body's prose leading. */
line-height: 1;
padding: .3rem .4rem;
margin-top: .4rem;
}
.wrap {
@@ -106,10 +120,11 @@ body {
.wrap.solo { display: block; }
}
/* Colour inherited: nothing lighter than --ink reaches 4.5:1 on the --field
this sits on, and at .8em it is small text. */
footer {
max-width: 68rem;
margin: .5rem auto 0;
color: #eef;
font-size: .8em;
}
@@ -121,6 +136,9 @@ footer {
h2, h3 { scroll-margin-top: 1rem; }
/* Top two levels only: a display face reads worse than the system font over prose. */
h1, h2 { font-family: "League Spartan", var(--body-font); font-weight: 700; }
h1 { font-size: 1.9em; margin: 0 0 .3em; line-height: 1.2; }
h2 { font-size: 1.45em; margin: 2.5em 0 .5em; padding-bottom: .2em; border-bottom: 2px solid var(--rule); }
h3 { font-size: 1.15em; margin: 2em 0 .4em; }

View File

@@ -17,7 +17,7 @@
<header class="masthead">
<img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier">
<div class="tagline">Open Source offline browser</div>
<div class="tagline">Free software offline browser</div>
</header>
<div class="wrap">

View File

@@ -17,7 +17,7 @@
<header class="masthead">
<img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier">
<div class="tagline">Open Source offline browser</div>
<div class="tagline">Free software offline browser</div>
</header>
<div class="wrap">

View File

@@ -17,7 +17,7 @@
<header class="masthead">
<img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier">
<div class="tagline">Open Source offline browser</div>
<div class="tagline">Free software offline browser</div>
</header>
<div class="wrap">

93
html/fonts/OFL.txt Normal file
View File

@@ -0,0 +1,93 @@
Copyright 2020 The League Spartan Project Authors (https://github.com/theleagueof/league-spartan)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

View File

@@ -13,12 +13,14 @@
</head>
<body>
<!-- doc-chrome:masthead -->
<a class="skip" href="#main">Skip to content</a>
<header class="masthead">
<img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier">
<div class="tagline">Open Source offline browser</div>
<div class="tagline">Free software offline browser</div>
</header>
<!-- /doc-chrome:masthead -->
<div class="wrap">
@@ -120,13 +122,13 @@ data-for="droid">Tap <b>Next</b></span> to create a project, or open one you alr
<figure>
<img data-for="win" src="img/guide-win-start.png" alt="WinHTTrack welcome pane" width="1040" height="744">
<img data-for="web" src="img/guide-web-start.png" alt="WebHTTrack welcome page" width="1024" height="388">
<img data-for="web" src="img/guide-web-start.png" alt="WebHTTrack welcome page" width="1024" height="387">
<img data-for="droid" src="img/guide-droid-start.png" alt="HTTrack for Android welcome screen" width="600" height="1300">
<figcaption>The welcome screen.</figcaption>
</figure>
<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">The dropdown shows the language the interface is already in.
Picking another one reloads the page in 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
@@ -141,7 +143,7 @@ together in the list.</p>
<figure>
<img data-for="win" src="img/guide-win-project-name.png" alt="WinHTTrack project name, category and base path" width="1040" height="744">
<img data-for="web" src="img/guide-web-project-name.png" alt="WebHTTrack project name, category and base path" width="1024" height="346">
<img data-for="web" src="img/guide-web-project-name.png" alt="WebHTTrack project name, category and base path" width="1024" height="345">
<img data-for="droid" src="img/guide-droid-project-name.png" alt="Android project name, category and base path" width="600" height="1300">
<figcaption>Project name, category, and where the files go.</figcaption>
</figure>
@@ -160,7 +162,7 @@ keep their links to each other.</p>
<figure>
<img data-for="win" src="img/guide-win-project-setup.png" alt="WinHTTrack action and web addresses" width="1040" height="744">
<img data-for="web" src="img/guide-web-project-setup.png" alt="WebHTTrack action and web addresses" width="1024" height="496">
<img data-for="web" src="img/guide-web-project-setup.png" alt="WebHTTrack action and web addresses" width="1024" height="495">
<img data-for="droid" src="img/guide-droid-project-setup.png" alt="Android web address and action" width="600" height="1300">
<figcaption>The addresses to copy, and what to do with them.</figcaption>
</figure>
@@ -215,7 +217,7 @@ port, you point your browser at it as a proxy and click the link or the submit b
usual, and the address the browser asks for is added to the project.</p>
<figure data-for="web">
<img data-for="web" src="img/guide-web-add-url.png" alt="WebHTTrack add-a-URL dialog" width="1024" height="634">
<img data-for="web" src="img/guide-web-add-url.png" alt="WebHTTrack add-a-URL dialog" width="1024" height="600">
<figcaption>Adding a single address, with an optional login.</figcaption>
</figure>
@@ -244,7 +246,7 @@ mirror straight away.</p>
<figure data-for="win web">
<img data-for="win" src="img/guide-win-ready.png" alt="WinHTTrack connection settings before starting" width="1040" height="744">
<img data-for="web" src="img/guide-web-ready.png" alt="WebHTTrack ready to start" width="1024" height="508">
<img data-for="web" src="img/guide-web-ready.png" alt="WebHTTrack ready to start" width="1024" height="489">
<figcaption>The last screen before the mirror runs.</figcaption>
</figure>
@@ -277,7 +279,7 @@ mirrored pages are rewritten to point at each other, so it browses offline in an
<figure>
<img data-for="win" src="img/guide-win-finished.png" alt="WinHTTrack mirror complete" width="1040" height="744">
<img data-for="web" src="img/guide-web-finished.png" alt="WebHTTrack mirror complete" width="1024" height="487">
<img data-for="web" src="img/guide-web-finished.png" alt="WebHTTrack mirror complete" width="1024" height="486">
<img data-for="droid" src="img/guide-droid-finished.png" alt="Android mirror complete" width="600" height="1300">
<figcaption>Done.</figcaption>
</figure>
@@ -441,7 +443,7 @@ bottom are the ones that keep you welcome.</p>
<figure>
<img data-for="win" src="img/guide-win-opt-flow-control.png" alt="WinHTTrack Flow Control tab" width="524" height="448">
<img data-for="web" src="img/guide-web-opt-flow-control.png" alt="WebHTTrack Flow Control tab" width="1024" height="910">
<img data-for="web" src="img/guide-web-opt-flow-control.png" alt="WebHTTrack Flow Control tab" width="1024" height="862">
<img data-for="droid" src="img/guide-droid-opt-flow-control.png" alt="Android Flow Control tab" width="600" height="1300">
</figure>
@@ -484,7 +486,7 @@ bottom are the ones that keep you welcome.</p>
<figure>
<img data-for="win" src="img/guide-win-opt-links.png" alt="WinHTTrack Links tab" width="524" height="448">
<img data-for="web" src="img/guide-web-opt-links.png" alt="WebHTTrack Links tab" width="1024" height="781">
<img data-for="web" src="img/guide-web-opt-links.png" alt="WebHTTrack Links tab" width="1024" height="749">
<img data-for="droid" src="img/guide-droid-opt-links.png" alt="Android Links tab" width="600" height="1300">
</figure>
@@ -521,7 +523,7 @@ bottom are the ones that keep you welcome.</p>
<figure>
<img data-for="win" src="img/guide-win-opt-build.png" alt="WinHTTrack Build tab" width="524" height="448">
<img data-for="web" src="img/guide-web-opt-build.png" alt="WebHTTrack Build tab" width="1024" height="986">
<img data-for="web" src="img/guide-web-opt-build.png" alt="WebHTTrack Build tab" width="1024" height="976">
<img data-for="droid" src="img/guide-droid-opt-build.png" alt="Android Build tab" width="600" height="1300">
</figure>
@@ -587,7 +589,7 @@ bottom are the ones that keep you welcome.</p>
<figure>
<img data-for="win" src="img/guide-win-opt-browser-id.png" alt="WinHTTrack Browser ID tab" width="524" height="448">
<img data-for="web" src="img/guide-web-opt-browser-id.png" alt="WebHTTrack Browser ID tab" width="1024" height="706">
<img data-for="web" src="img/guide-web-opt-browser-id.png" alt="WebHTTrack Browser ID tab" width="1024" height="654">
<img data-for="droid" src="img/guide-droid-opt-browser-id.png" alt="Android Browser ID tab" width="600" height="1300">
</figure>
@@ -627,7 +629,7 @@ bottom are the ones that keep you welcome.</p>
<figure>
<img data-for="win" src="img/guide-win-opt-spider.png" alt="WinHTTrack Spider tab" width="524" height="448">
<img data-for="web" src="img/guide-web-opt-spider.png" alt="WebHTTrack Spider tab" width="1024" height="1557">
<img data-for="web" src="img/guide-web-opt-spider.png" alt="WebHTTrack Spider tab" width="1024" height="1541">
<img data-for="droid" src="img/guide-droid-opt-spider.png" alt="Android Spider tab" width="600" height="1300">
</figure>
@@ -714,7 +716,7 @@ bottom are the ones that keep you welcome.</p>
<figure>
<img data-for="win" src="img/guide-win-opt-proxy.png" alt="WinHTTrack Proxy tab" width="524" height="448">
<img data-for="web" src="img/guide-web-opt-proxy.png" alt="WebHTTrack Proxy tab" width="1024" height="762">
<img data-for="web" src="img/guide-web-opt-proxy.png" alt="WebHTTrack Proxy tab" width="1024" height="760">
<img data-for="droid" src="img/guide-droid-opt-proxy.png" alt="Android Proxy tab" width="600" height="1300">
</figure>
@@ -751,7 +753,7 @@ bottom are the ones that keep you welcome.</p>
<figure>
<img data-for="win" src="img/guide-win-opt-log-index-cache.png" alt="WinHTTrack Log, Index, Cache tab" width="524" height="448">
<img data-for="web" src="img/guide-web-opt-log-index-cache.png" alt="WebHTTrack Log, Index, Cache tab" width="1024" height="1278">
<img data-for="web" src="img/guide-web-opt-log-index-cache.png" alt="WebHTTrack Log, Index, Cache tab" width="1024" height="1266">
<img data-for="droid" src="img/guide-droid-opt-log-index-cache.png" alt="Android Log, Index, Cache tab" width="600" height="1300">
</figure>
@@ -850,7 +852,7 @@ bottom are the ones that keep you welcome.</p>
<figure>
<img data-for="win" src="img/guide-win-opt-experts-only.png" alt="WinHTTrack Experts Only tab" width="524" height="448">
<img data-for="web" src="img/guide-web-opt-experts-only.png" alt="WebHTTrack Experts Only tab" width="1024" height="1088">
<img data-for="web" src="img/guide-web-opt-experts-only.png" alt="WebHTTrack Experts Only tab" width="1024" height="1085">
<img data-for="droid" src="img/guide-droid-opt-experts-only.png" alt="Android Experts Only tab" width="600" height="1300">
</figure>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -10,12 +10,14 @@
</head>
<body>
<!-- doc-chrome:masthead -->
<a class="skip" href="#main">Skip to content</a>
<header class="masthead">
<img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier">
<div class="tagline">Open Source offline browser</div>
<div class="tagline">Free software offline browser</div>
</header>
<!-- /doc-chrome:masthead -->
<div class="wrap solo">
<main id="main">

View File

@@ -17,7 +17,7 @@
<header class="masthead">
<img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier">
<div class="tagline">Open Source offline browser</div>
<div class="tagline">Free software offline browser</div>
</header>
<div class="wrap">

View File

@@ -17,7 +17,7 @@
<header class="masthead">
<img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier">
<div class="tagline">Open Source offline browser</div>
<div class="tagline">Free software offline browser</div>
</header>
<div class="wrap">

View File

@@ -17,7 +17,7 @@
<header class="masthead">
<img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier">
<div class="tagline">Open Source offline browser</div>
<div class="tagline">Free software offline browser</div>
</header>
<div class="wrap">

View File

@@ -17,7 +17,7 @@
<header class="masthead">
<img src="images/wordmark.svg" width="400" height="36" alt="HTTrack Website Copier">
<div class="tagline">Open Source offline browser</div>
<div class="tagline">Free software offline browser</div>
</header>
<div class="wrap">

View File

@@ -43,13 +43,29 @@
</developer>
<screenshots>
<screenshot type="default">
<caption>Choosing the addresses and options for a new mirror</caption>
<image>https://www.httrack.com/html/images/screenshot_01b.jpg</image>
<caption>Choosing which files a mirror includes</caption>
<!-- A guide asset, so every screenshot reshoot refreshes this too.
appstream-network.yml resolves the URL weekly if the name moves.
Not a wizard pane: the walk crawls a loopback server, so every pane
showing the address advertises 127.0.0.1 and an ephemeral port. -->
<image>https://www.httrack.com/html/img/guide-web-opt-scan-rules.png</image>
</screenshot>
</screenshots>
<content_rating type="oars-1.1"/>
<!-- Newest first; tests/01_engine-version-macros.test enforces it. -->
<releases>
<release version="3.49.20" date="2026-08-10">
<description>
<ul>
<li>A link pointing to another site is no longer mistaken for a page of your own and mirrored in full</li>
<li>A mirror told to quit while an FTP file is being received now stops, instead of hanging</li>
<li>Stopping an FTP download no longer closes its connection twice, which could interrupt another download</li>
<li>The welcome page's language menu shows the language in use</li>
<li>The macOS download says in its name that it is for Apple Silicon</li>
<li>Refreshed screenshots and typography throughout the documentation</li>
</ul>
</description>
</release>
<release version="3.49.19" date="2026-08-08">
<description>
<ul>

View File

@@ -105,8 +105,7 @@ ${LANG_THANKYOU}!
<tr><td align="right">
${LANG_P15}
<select name="lang" onChange="form.redirect.value='index.html'; form.submit()">
<option value=0></option>
${list:#iso}
${listid:lang:#iso}
</select>
</td></tr>

View File

@@ -6,8 +6,15 @@ body, td {
font: 14px "Trebuchet MS", Verdana, Arial, Helvetica, sans-serif;
}
/* Take back the whitespace the SVG carries around the letters, so the bar sits on
the wordmark's baseline as it does on the doc pages (html/doc.css). */
#title {
display: block;
margin: -1.7px 0 -1.1px;
}
#subTitle {
background: #000; color: #fff; padding: 4px; font-weight: bold;
background: #000; color: #fff; padding: 4px; font-weight: bold;
}
.tabCtrl {

View File

@@ -1693,6 +1693,21 @@ int httpmirror(char *url1, httrackp * opt) {
// traitement (parsing)
// ------------------------------------------------------
/* An asset URL on a foreign host that answers an HTML page gets no depth:
its links are same-host, and would mirror that whole site (#121). */
if (!is_binary && heap(ptr)->depth > 0 &&
is_html_mime_type(r.contenttype) &&
hts_link_is_foreign_asset(opt, ptr)) {
hts_log_print(
opt, LOG_INFO,
"Note: not scanning %s%s, an HTML page on a host foreign "
"to %s; mirror it by adding it as a starting URL",
/* normalized: a page-supplied link can carry credentials */
jump_identification_const(urladr()), urlfil(),
jump_identification_const(heap(heap(ptr)->precedent)->adr));
heap(ptr)->depth = 0;
}
// traiter
if (!is_binary && ((is_hypertext_mime(opt, r.contenttype, urlfil())) /* Is HTML or Js, .. */
||(may_be_hypertext_mime(opt, r.contenttype, urlfil()) && r.adr != NULL) /* Is real media, .. */

View File

@@ -56,8 +56,10 @@ Please visit our Website: http://www.httrack.com
#endif
#endif
// ftp mode passif
/* Passive FTP; overridable so CI can compile the active branch (#1091). */
#ifndef FTP_PASV
#define FTP_PASV 1
#endif
#define FTP_DEBUG 0
@@ -314,6 +316,36 @@ static hts_boolean ftp_connect(lien_back *back, T_SOC soc, SOCaddr *server,
return socket_set_nonblocking(soc, HTS_FALSE);
}
#if !FTP_PASV
/* The active-mode counterpart: an unbounded accept() waits for a data
connection the server may never make (#1074). */
static T_SOC ftp_accept(lien_back *back, T_SOC soc, int timeout,
const httrackp *opt) {
const TStamp started = time_local();
T_SOC soc_dat;
timeout = ftp_wait_left(opt, timeout);
if (!socket_set_nonblocking(soc, HTS_TRUE))
return INVALID_SOCKET;
/* state.stop counts as in ftp_connect: nothing is established yet */
while (check_socket(soc) == 0) {
if (back->stop_ftp || (opt != NULL && opt->state.stop) ||
(int) (time_local() - started) >= timeout)
return INVALID_SOCKET;
Sleep(100);
}
soc_dat = accept(soc, NULL, NULL);
/* BSD hands the listener's non-blocking flag down to the accepted socket,
which the transfer below reads blocking. */
if (soc_dat != INVALID_SOCKET &&
!socket_set_nonblocking(soc_dat, HTS_FALSE)) {
deletesoc(soc_dat);
return INVALID_SOCKET;
}
return soc_dat;
}
#endif
/* Resolve, likewise: the shared resolver bounds itself by opt->timeout alone
and takes no stop flag. */
static SOCaddr *ftp_dns_resolve(httrackp *opt, lien_back *back,
@@ -817,8 +849,8 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
get_ftp_line(back, soc_ctl, line, sizeof(line), timeout, opt);
_CHECK_HALT_FTP;
if (line[0] == '1') {
//T_SOC soc_dat;
if ((soc_dat = accept(soc_servdat, NULL, NULL)) == INVALID_SOCKET) {
if ((soc_dat = ftp_accept(back, soc_servdat, timeout, opt)) ==
INVALID_SOCKET) {
strcpybuff(back->r.msg, "Unable to accept connection");
back->r.statuscode = STATUSCODE_INVALID;
}
@@ -830,11 +862,9 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
htsblk_failf(&back->r, "PORT command error: %s", linejmp(line));
back->r.statuscode = STATUSCODE_INVALID;
}
#ifdef _WIN32
closesocket(soc_servdat);
#else
close(soc_servdat);
#endif
/* invalidate, or the _CHECK_HALT_FTP below re-closes a stale fd */
deletesoc(soc_servdat);
soc_servdat = INVALID_SOCKET;
} else {
strcpybuff(back->r.msg, "Unable to listen to a port");
back->r.statuscode = STATUSCODE_INVALID;
@@ -931,11 +961,9 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
strcpybuff(back->r.msg, "Unable to write file");
back->r.statuscode = STATUSCODE_INVALID;
}
#ifdef _WIN32
closesocket(soc_dat);
#else
close(soc_dat);
#endif
/* invalidate, or the _CHECK_HALT_FTP below re-closes a stale fd */
deletesoc(soc_dat);
soc_dat = INVALID_SOCKET;
// 226 Transfer complete?
if (back->r.statuscode != -1) {

View File

@@ -43,8 +43,8 @@ Please visit our Website: http://www.httrack.com
configure.ac, decoupled from these). VERSION is the display form, VERSIONID
the dotted numeric form, AFF_VERSION the short form shown in footers,
LIB_VERSION the data/cache format generation. */
#define HTTRACK_VERSION "3.49-19"
#define HTTRACK_VERSIONID "3.49.19"
#define HTTRACK_VERSION "3.49-20"
#define HTTRACK_VERSIONID "3.49.20"
#define HTTRACK_AFF_VERSION "3.x"
#define HTTRACK_LIB_VERSION "2.0"

View File

@@ -4119,6 +4119,12 @@ void hts_mirror_process_user_interaction(htsmoduleStruct * str,
XH_uninit;
return;
}
/* Same omission as the wait for the current link: with every slot busy
the mirror parks here instead, and the exit never lands (#1096). */
if (*stre->exit_xh_) {
XH_uninit;
return;
}
Sleep(100); // pause
}
opt->state._hts_in_html_parsing = prev;
@@ -4226,6 +4232,12 @@ int hts_mirror_wait_for_next_file(htsmoduleStruct * str,
XH_uninit;
return 0;
}
/* An exit asked of the engine must leave this wait too: only teardown
stops a live FTP worker, so waiting on its slot never ends (#1096). */
if (*stre->exit_xh_) {
XH_uninit;
return 0;
}
// And fill the backing stack
if (back[b].status > 0)
back_fillmax(sback, opt, cache, ptr, numero_passe);

View File

@@ -7874,6 +7874,9 @@ static void st_backstop_slot(struct_back *sback, int p, int status,
sback->connect_fallback[p].addr_count = 1;
}
/* A network that rejects TEST-NET-1 instead of dropping it answers in ms. */
#define ST_BACKSTOP_SETTLE_MS 500
/* Refill every slot: a fresh pending connect for each state that owns a socket,
plus the poison the assertions read back. False if the blackhole answered. */
static hts_boolean st_backstop_arm(httrackp *opt, struct_back *sback,
@@ -7893,6 +7896,17 @@ static hts_boolean st_backstop_arm(httrackp *opt, struct_back *sback,
}
st_backstop_slot(sback, i, status[i], &r[i]);
}
/* The readback above is too early to see a refusal, and back_wait() then
ends slots the caller asserts are intact (the powerpc buildds). */
Sleep(ST_BACKSTOP_SETTLE_MS);
for (i = 0; i < slots; i++) {
if (i != dnsslot && check_socket_connect(sback->lnk[i].r.soc) != 0) {
printf("backstop: SKIP (the network answered for " ST_BACKSTOP_DEADHOST
" within %d ms)\n",
ST_BACKSTOP_SETTLE_MS);
return HTS_FALSE;
}
}
return HTS_TRUE;
}

View File

@@ -1509,7 +1509,11 @@ int smallserver(T_SOC soc, char *url, char *method, char *data, char *path) {
StringClear(tmpbuff);
if (format == 2) {
StringCat(output, "<option value=1>");
/* Check the default here too: the loop only opens
* ids 2 and up. */
StringCat(output, listDefault == 1
? "<option value=1 selected>"
: "<option value=1>");
} else if (format == -2) {
StringCat(output, "<option value=\"");
}

View File

@@ -156,7 +156,14 @@ HTS_STATIC void StringOom_(size_t size) {
do { \
const size_t capacity_ = (size_t) (CAPACITY); \
while ((BLK).capacity_ < capacity_) { \
const size_t newcap_ = (BLK).capacity_ < 16 ? 16 : (BLK).capacity_ * 2; \
const size_t maxcap_ = (size_t) -1; \
const size_t mincap_ = 16; \
const size_t cur_ = (BLK).capacity_; \
/* Past the halfway point cur_*2 wraps below cur_, which no further \
doubling climbs back out of; ask for the requested total instead. */ \
const size_t newcap_ = cur_ < mincap_ ? mincap_ \
: cur_ > maxcap_ / 2 ? capacity_ \
: cur_ * 2; \
char *const buff_ = STRING_REALLOC((BLK).buffer_, newcap_); \
\
if (buff_ == NULL) { \

View File

@@ -930,6 +930,24 @@ int hts_acceptmime(httrackp * opt, int ptr,
#undef _ROBOTS
}
hts_boolean hts_link_is_foreign_asset(httrackp *opt, int ptr) {
int parent;
if (ptr <= 0)
return HTS_FALSE;
parent = heap(ptr)->precedent;
/* Seeds hang off the synthetic primary link 0: the user asked for those. */
if (parent <= 0 || parent >= opt->lien_tot)
return HTS_FALSE;
if (ishtml(opt, heap(ptr)->fil) != 0) /* hypertext, or no telling */
return HTS_FALSE;
/* adr carries the scheme and any user:pw@, so compare bare authorities. */
return strfield2(jump_identification_const(heap(ptr)->adr),
jump_identification_const(heap(parent)->adr))
? HTS_FALSE
: HTS_TRUE;
}
// tester taille
int hts_testlinksize(httrackp * opt, const char *adr, const char *fil, LLint size) {
int jok = 0;
@@ -954,7 +972,7 @@ int hts_testlinksize(httrackp * opt, const char *adr, const char *fil, LLint siz
lfull[0] = '\0';
strcatbuff(lfull, adr);
if (*fil != '/')
strcatbuff(l, "/");
strcatbuff(lfull, "/");
strcatbuff(lfull, fil);
// filters, 0=sait pas 1=ok -1=interdit

View File

@@ -69,6 +69,10 @@ typedef struct htspair_t {
/* HTS_TRUE if tag starts with the whole token cmp; NULL tag never matches. */
hts_boolean hts_cmp_tag_token(const char *tag, const char *cmp);
/* HTS_TRUE when link `ptr` was taken for an asset on a host foreign to its
referer: its URL does not look like hypertext, and the hosts differ. */
hts_boolean hts_link_is_foreign_asset(httrackp *opt, int ptr);
int hts_acceptlink(httrackp * opt, int ptr,
const char *adr, const char *fil,
const char *tag, const char *attribute,

View File

@@ -17,8 +17,8 @@
#endif
VS_VERSION_INFO VERSIONINFO
FILEVERSION 3, 49, 19, 0
PRODUCTVERSION 3, 49, 19, 0
FILEVERSION 3, 49, 20, 0
PRODUCTVERSION 3, 49, 20, 0
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
#ifdef _DEBUG
FILEFLAGS VS_FF_DEBUG
@@ -35,12 +35,12 @@ BEGIN
BEGIN
VALUE "CompanyName", "Xavier Roche"
VALUE "FileDescription", VER_FILE_DESCRIPTION
VALUE "FileVersion", "3.49.19"
VALUE "FileVersion", "3.49.20"
VALUE "InternalName", VER_ORIGINAL_FILENAME
VALUE "LegalCopyright", "Copyright (C) 1998-2026 Xavier Roche and other contributors. GNU GPL v3 or later."
VALUE "OriginalFilename", VER_ORIGINAL_FILENAME
VALUE "ProductName", "HTTrack Website Copier"
VALUE "ProductVersion", "3.49-19"
VALUE "ProductVersion", "3.49-20"
END
END
BLOCK "VarFileInfo"

View File

@@ -3,8 +3,11 @@
set -euo pipefail
# shellcheck source=tests/testlib.sh
. "$(dirname "$0")/testlib.sh"
# Cache-index (.ndx) parse must stay inside the buffer on a corrupt length
# prefix (ASan aborts here on the pre-fix binary; the OK check gates the
# non-sanitized build too).
test "$(httrack -O /dev/null -#test=cacheindex)" == 'cacheindex: OK'
assert_selftest 'cacheindex: OK' cacheindex

View File

@@ -3,13 +3,11 @@
set -euo pipefail
testdir=$(cd "$(dirname "$0")" && pwd)
# shellcheck source=tests/testlib.sh
. "${testdir}/testlib.sh"
. "$(dirname "$0")/testlib.sh"
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/httrack_changes_st.XXXXXX") || exit 1
trap 'set +e; rm -rf "$tmpdir"' EXIT
trap 'rm -rf "$tmpdir"' HUP INT QUIT PIPE TERM
cleanup_push rm -rf "$tmpdir"
# --changes bucket accounting and JSON escaping (#714).
expect_ok "changes self-test" httrack -O "${tmpdir}/o1" -#test=changes run

View File

@@ -3,14 +3,15 @@
set -euo pipefail
# shellcheck source=tests/testlib.sh
. "$(dirname "$0")/testlib.sh"
# charset -> UTF-8 conversion (hts_convertStringToUTF8).
# -#test=charset <charset> <string> prints the string re-decoded from <charset> as UTF-8.
conv() {
test "$(httrack -O /dev/null -#test=charset "$1" "$2")" == "$3" || exit 1
}
conv() { assert_selftest "$3" charset "$1" "$2"; }
# crash probe: malformed input must exit cleanly, not abort.
runs() {
httrack -O /dev/null -#test=charset "$1" "$2" >/dev/null 2>&1 || exit 1
httrack -O /dev/null -#test=charset "$1" "$2" >/dev/null 2>&1 || fail "charset '$1' '$2' exited non-zero"
}
# the source bytes below are UTF-8 (this file is UTF-8); "café" is 0x63 61 66 C3 A9.
@@ -31,7 +32,7 @@ conv 'us-ascii' 'hello' 'hello'
# unknown charset: ASCII passes through unchanged, but non-ASCII input cannot be
# decoded and yields empty output (an error is printed to stderr).
conv 'no-such-charset-xyz' 'abc' 'abc'
test "$(httrack -O /dev/null -#test=charset 'no-such-charset-xyz' 'café' 2>/dev/null)" == "" || exit 1
assert_eq "" "$(httrack -O /dev/null -#test=charset 'no-such-charset-xyz' 'café' 2>/dev/null)"
# malformed UTF-8 (lone continuation byte, truncated lead byte) must not crash
runs 'utf-8' 'hex:80'
@@ -39,9 +40,7 @@ runs 'utf-8' 'hex:c3'
# hts_getCharsetFromMeta: <meta> charset extraction, HTML5 and legacy forms.
# -#test=metacharset <html> prints the charset, or "(none)".
meta() {
test "$(httrack -O /dev/null -#test=metacharset "$1")" == "$2" || exit 1
}
meta() { assert_selftest "$2" metacharset "$1"; }
# HTML5 form, quoting/spacing flavors
meta '<meta charset="utf-8">' 'utf-8'
@@ -50,7 +49,7 @@ meta '<meta charset=utf-8>' 'utf-8'
meta '<meta charset=utf-8 />' 'utf-8'
meta '<meta charset=utf-8/>' 'utf-8'
# charset labels are case-insensitive downstream; only require case-insensitive match
test "$(httrack -O /dev/null -#test=metacharset '<META CHARSET=UTF-8>' | tr '[:upper:]' '[:lower:]')" == 'utf-8' || exit 1
assert_eq 'utf-8' "$(httrack -O /dev/null -#test=metacharset '<META CHARSET=UTF-8>' | tr '[:upper:]' '[:lower:]')"
meta '<html><head><meta charset = "utf-8" ></head>' 'utf-8'
# legacy http-equiv form, attributes in any order
@@ -83,9 +82,7 @@ meta '<meta ' '(none)'
meta '<meta' '(none)'
# hts_isStringUTF8: valid-UTF-8 probe (guards the #180 link conversion).
utf8() {
test "$(httrack -O /dev/null -#test=isutf8 "$1")" == "$2" || exit 1
}
utf8() { assert_selftest "$2" isutf8 "$1"; }
utf8 'plain' '1'
utf8 'café' '1'
utf8 '统计' '1'

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