Compare commits

..

64 Commits

Author SHA1 Message Date
Xavier Roche
f7d45027a8 Merge branch 'master' into fix/proxytrack-dav-default-doc
Union both sides' expected-skip entries and TESTS-tail additions,
deduplicating and restoring numeric order.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-27 17:32:15 +02:00
Xavier Roche
8c5b208fac A 304 during --update leaks the response's header buffer (#808)
* A 304 during --update leaks the whole previous htsblk

back_wait() handles a 304 by replacing the response struct with the cache
entry, carrying only the socket and keep-alive members across via
back_connxfr(). The struct assignment drops every owned pointer the live
response still held without freeing any of them: the 8 KB header buffer on
every update, plus the two WARC header stashes when --warc-file is on. An
update over a 10k-page site drops roughly 80 MB in one run.

back_clear_entry() already knew how to tear those down, so the frees move into
a helper that both it and the 304 path call.

The new test runs the two-pass mini304 crawl with LeakSanitizer on, which the
sanitized CI job otherwise disables. The fresh first pass is the control: it
has no cache entry to read back and is clean either way. The update pass
reports 16 KB in 2 objects on master, one per unchanged URL, and nothing with
the fix.

Closes #782

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

* Trim the test header

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

* Review fixes: reuse deleteaddr(), and cover the WARC limb

back_free_response() was reimplementing deleteaddr(), which already frees adr
and headers and NULLs both; call it instead so the two cannot drift.

Test 114 never passed --warc-file, so the warc_free_request() limb ran with
both pointers NULL on every path it exercised and deleting it kept the test
green. A third pass turns the archive on, and it now fails with the 835 and 238
byte stashes when that call goes away.

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

* Register the new leak test as an expected Windows skip

The Win32/x64 job pins the exact set of tests allowed to skip, so an
all-skipped suite cannot report green. 114_local-update-304-leak needs a
LeakSanitizer build and MSVC has no equivalent, so it skips there and tripped
the gate with fail=0.

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 17:18:44 +02:00
Xavier Roche
b7cff7aeee hts_rename_over() can lose its destination when the retried rename fails (#816)
* hts_rename_over() can lose its destination when the retried rename fails

The unlink-then-rename fallback leaves nothing in place of dst between
the unlink and the retry, so a retry that fails too loses it. Park dst
under a free scratch name instead, drop it once the move succeeded, and
put it back otherwise.

Closes #790

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

* Review fixes: check the restore, probe in UTF-8, state the honest guarantee

The move back out of the parked name was unchecked, so a retry that
failed for a reason that still applied left dst absent with the content
orphaned under a name nothing reported. Check it, retry once, and name
the parked copy in the log; hts_rename_over() takes an httrackp for that.

The aside probe used fexist(), which is not UTF-8 and consults the ANSI
codepage on Windows while the renames beside it are wide. It also reads
a directory as a free name, so the park now skips a name whose rename
refuses rather than giving up on it.

The header claimed a failure leaves dst as it was, which the crash
window between the two renames does not give.

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

* The aside fallback parked a directory that stood in the way

Windows refuses every rename onto an existing target, so the fallback is
production code there rather than the rare path it is on POSIX. A
directory at the destination was renamed aside like a file, the move
then succeeded, and UNLINK could not drop the parked directory, so the
call reported success where master had reported failure and left an
orphan behind. 101_local-update-stale-bak plants exactly that shape and
caught it on both Windows legs.

Park a regular file only. A directory in the way is refused, as before.

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 16:51:19 +02:00
Xavier Roche
bc8b6cfa00 A successful FTP transfer was blanked when its backlog slot was swapped out (#809)
The FTP worker writes url_sav itself, so its slot carries a size but no
in-memory body. Serializing that slot to the on-disk ready table stores no
body, and the read-back took the size from what it stored, leaving zero: the
link writer then saw an empty response and created a 0-byte file over the
bytes already on disk, while the engine logged the transfer as a success.

Test 110 mirrors twelve files at -c8, which is what makes a ready slot wait
long enough to be swapped, and -#test=backswap covers the round-trip directly.

Closes #797

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 16:43:43 +02:00
Xavier Roche
260cef25fe PT_GetTime hands out gmtime's shared static instead of a reentrant breakdown (#805)
* PT_GetTime copied gmtime's shared static instead of a reentrant breakdown

On _WIN32 the success path took gmtime()'s pointer and dereferenced it after
the fact, so a concurrent conversion on another thread could change the
breakdown under it. The POSIX branch was already reentrant via gmtime_r, and
the same #ifdef pair had been copy-pasted into hts_now_iso8601() and the WARC
auto-name; fold all of them onto one hts_gmtime() helper, and give ProxyTrack's
WebDAV listing the same treatment, since it read the static's fields well past
the call.

Windows uses Microsoft's gmtime_s (destination first, errno_t return), not the
C11 Annex K function of the same name.

Covered by a new "gmtime" engine self-test: a reference table checks the
breakdown itself, which is what catches a swapped-argument call on the MSVC
leg, and eight threads hammering the helper catch a return to the shared
static (16k of 400k conversions corrupt with that mutant in place).

Closes #794

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

* Give the gmtime self-test teeth on the failure path and off UTC

Three holes the test-design audit found by running mutants rather than
reading the diff.

A helper that discarded gmtime_r's NULL and always claimed success passed
every phase, yet that boolean is the only failure signal hts_now_iso8601,
warc_open and PT_GetTime have; all three would have formatted an
uninitialised struct tm. A forced-failure row now converts INT64_MAX, gated
on a 64-bit time_t.

The localtime_r mutant only died on a non-UTC box. CI runners are UTC, where
localtime_r and gmtime_r agree on every reference row, so the test exports
TZ=XXX5.

The "first result survives the second call" phase could not fail: both
buffers are caller-owned stack storage no implementation writing through
tmbuf could disturb. Removed rather than left reading as coverage.

expect_ok() was a third byte-identical copy; it moves to tests/testlib.sh
with the two existing callers.

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

* Rename the self-test's out-of-range time_t off the "far" keyword

WinDef.h defines "far" away to nothing, so the declaration lost its variable
and MSVC rejected the file. Both Windows legs caught it; the POSIX builds
never see the macro.

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 16:21:40 +02:00
Xavier Roche
bd60dac914 Handle the fgets return in the log-callback self-test (#827)
(void) does not suppress glibc's warn_unused_result, so gcc warned on
the read-back in st_logcallback. Treat a failed read as the test failure
it is instead of asserting against an empty buffer.

Closes #812

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 16:06:24 +02:00
Xavier Roche
0984aa2530 grep -q SIGPIPEs the producer feeding it, so an assertion that held reports failure (#822)
* Match captured output with a here-string, not a pipe into grep -q

grep -q exits on the first match, so whatever the producer still had to
write takes SIGPIPE; under pipefail that becomes the pipeline's status and
an assertion that held reports failure. bash issues one write() per line,
so any match that is not on the last line is exposed.

Converts every test assertion whose producer is a shell builtin or shell
function, including two pipelines used as an if condition where the SIGPIPE
silently flips the branch. Generalizes the AGENTS.md bullet, which only
covered the "&& fail" spelling.

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

* Convert the remaining pipe-into-grep -q sites

The self-test drivers survive only because the matched line is the last
thing httrack prints; one added line of output turns a pass into 141.
Nothing in tests/ pipes into grep -q now.

The two zlib drivers claimed the harness might run them under a POSIX
/bin/sh: it does not. configure resolves $(BASH) to bash, test-timeout.sh
execs it, and 01_zlib-warc-wacz.test already uses "set -o pipefail" (which
dash lacks) on the macOS leg. Kept the half that is true, BSD tool flags.

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 15:49:00 +02:00
Xavier Roche
13953867e8 Merge remote-tracking branch 'origin/master' into fix/proxytrack-dav-default-doc
Signed-off-by: Xavier Roche <roche@httrack.com>

# Conflicts:
#	tests/Makefile.am
2026-07-27 15:27:58 +02:00
Xavier Roche
bc6c53598c The -o help text promises a generated error page the engine never builds (#803)
* The -o help text promises a generated error page the engine never builds

`-o` only decides whether the error page the server sent survives: `store_errpage`
keeps `r.adr` alive so the normal save path writes it, and the `-o0` arm frees it.
Nothing anywhere builds a stand-in body. The one block that would have was dead
since the 3.20.2 import and was removed in #783.

Reword the help line, the man page and fcguide's two `-o` prose blocks to say the
server's error page is saved rather than generated, and extend 23_local-errpage
so the mirrored 404 has to carry the server's own body.

Closes #787

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

* Condense the -o1 control comment to one line

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Signed-off-by: Xavier Roche <xroche@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 15:26:04 +02:00
Xavier Roche
809d5b6ffc An FTP --update resumed a complete mirror with REST and spliced the old file into the new body (#810)
* An FTP --update resumed a complete mirror with REST and spliced the old file into the new body

FTP sent REST whenever the mirrored file merely existed, and on an --update
pass every previously mirrored file exists, so a complete copy was treated as
an interrupted download. The server resumed at its length and the mirror ended
up part old body, part new tail, at exactly the remote size, so nothing
downstream noticed. Resuming now follows the decision back_add() already makes
for HTTP, which only marks a copy partial when the cache does not hold it, and
r.size is seeded from the resume offset so a genuine resume is no longer
reported "FTP file incomplete".

Closes #798

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

* Format the two comment lines the earlier pass missed

git-clang-format only sees the diff present when it runs; the comments were
translated after it, so those lines never went through it.

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 15:21:50 +02:00
Xavier Roche
db89c82d79 Pin the new skip on Windows and tighten the listing assertions
The Windows job runs *_local-*.test and compares the skip list against an
exact string, so an unpinned skip fails the leg with fail=0, which reads
like a flake. Assert the href and the response count too: displayname
alone comes from the href's trailing component, so it cannot see a wrong
path above it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-27 15:11:55 +02:00
Xavier Roche
ad3f051ac6 A PROPFIND on an exact cache entry crashes proxytrack
PT_Enumerate() reports a folder's default document as a zero-length name,
and the WebDAV listing loop read thisUrl[thisUrlLen - 1] on it, four
gigabytes past the string. One unauthenticated request took the whole
listener down.

Closes #828

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-27 14:40:41 +02:00
Xavier Roche
536d7515ed CI format check compares against master's tip, not the branch's merge base (#802)
The changed-lines clang-format job resolved its base as origin/<base_ref>,
which is master's tip when the job runs. Once master gains a C commit while
a PR is open, the comparison also picks up the reverse of that commit and
the job fails on code the PR never touched.

Use git merge-base instead, and fail loudly if there is none rather than
falling back to a whole-tree comparison.

Closes #800

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 14:07:45 +02:00
Xavier Roche
374087a81c A log callback consumes the va_list the log file still needs (#801)
hts_log_vprint() makes a va_copy and then throws it away: the callback is handed
the original args, and vfprintf() writes the log file from that same,
already-consumed list. On x86_64 a va_list is a one-element array, so the callee
moves the caller's cursor; the second traversal reads past the register save
area, and a %s yields a junk pointer that vfprintf() dereferences.

Only an embedder that installs a callback is affected, so the CLI never sees it.
HTTrack Android does: --sitemap is the first option whose LOG_NOTICE lines carry
arguments, and ticking its checkbox segfaults the crawl thread. The same crawl is
clean under the CLI built with ASan+UBSan.

Pass the copy to the callback. -#test=logcallback sends one line with a %d and a
%s through both sinks and compares them; on the unfixed code the log file gets
"0 " and the test fails. It also logs a second line below opt->debug with no
opt->log, pinning that the callback fires above the level filter.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 13:48:00 +02:00
Xavier Roche
83079b788d Teardown must not decide a test's verdict (#792)
Under `set -e` a failing command in an EXIT trap becomes the script's exit status, so a hiccup while tearing down fixtures fails a test whose assertions all passed. That is what turned `57_local-proxy-connect.test` red on the Windows x64 leg of #765: five OK lines, no FAIL, exit 1. Every EXIT trap in the suite now runs teardown with errexit off, and the signal traps keep their own `trap` line, since sharing `set +e` with HUP/INT/QUIT/PIPE/TERM would leave errexit off for the rest of a signalled run and let a torn-down test still report success.

A `|| true` on the `rm` would have been smaller, but it throws away the only diagnostic, and the evidence does not say which teardown command failed: a blocked `rm -rf` on the Windows runner exits 1 and prints "Device or resource busy", while the log shows exit 1 and nothing at all. The sharing violation in the issue is the plausible mechanism rather than a confirmed one, so whatever it really is now prints its own error.

`99_teardown-status.test` pins the semantics both ways and scans the suite so a new test cannot reintroduce the shape, the leaky combined trap included. The `return 0` that three `cleanup()` bodies ended with never protected anything, since errexit fires at the failing command before it is reached.

Closes #773

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 13:24:23 +02:00
Xavier Roche
1fa3cb4e74 A wedged test hangs the job until CI cancels it, discarding the log that would name it (#796)
A test that wedges runs until CI cancels the step, and a cancelled step keeps neither its log nor the artifacts its `if: always()` uploads would have produced. That is why nobody has ever been able to say which Windows test hangs, across 19 dead jobs in the last day alone (#795).

Each test now runs under a wall-clock budget at the automake harness level, so an overrun names the test, dumps the surviving process tree and an engine stack, and exits 124. The step then fails rather than being cancelled, which is what keeps the log. Every POSIX `make check` leg gets this; the Windows leg runs its own serial loop and now calls the same wrapper. The budget is 600s, the value the Windows leg already used: it has to clear the 540s a three-pass crawl may legitimately take under `local-crawl.sh`'s own 180s-per-pass watchdogs, against a slowest healthy test that actually measures 39s.

Two things sit on top of the per-test bound. The Windows suite gives up at 25 minutes so it fails on its own terms well before the 45-minute step timeout, and it sweeps leaked engine processes between tests, naming whichever test left them. An orphaned `httrack.exe` starving the runner is the leading theory for the hang, and that sweep is what would confirm it.

This also fixes an unbounded `wait` after `kill_tree` in testlib.sh. When the kill failed to reap, which is exactly the native-Windows case those watchdogs exist for, the watchdog blocked forever and never printed the timeout it was about to report.

Stacks differ by platform, and each branch says which one it took, because a dump that silently produces nothing reads as coverage. Linux sends SIGABRT and lets httrack's own crash handler symbolize itself, verified against a real wedged crawl where it named `back_wait` at htsback.c:2710. macOS cannot do that, since htsbacktrace.c gates the handler on `__linux`, so it uses `sample(1)`. Windows uses `cdb` from the SDK, and that is the one path I could not exercise from here; it probes for the binary, reports when it is absent, and is bounded so a debugger that wedges cannot become the new hang.

Does not fix #795, only makes it diagnosable.

The between-test sweep costs the Windows step about two minutes (506s before, 619s on this run). That buys naming whichever test leaks, which is the whole lead on #795; it can be narrowed or dropped once the leak is found.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 13:06:51 +02:00
Xavier Roche
bab4b70a71 server-not-modified revisit records name no capture, so replay cannot resolve them (#789)
A 304 revisit record carried a `WARC-Profile` and nothing else naming what it stood for. Neither replay engine reads that field: pywb resolves in `_load_different_url_payload` on `WARC-Refers-To-Target-URI` and raises `ArchiveLoadFailed` without it, wabac.js reads `warcRefersToTargetURI` and otherwise answers Not Found. So the records were conformant under WARC 1.1 6.7.3, which only recommends the field, and unreplayable in both engines that matter.

For a server-not-modified revisit the referred-to URI is the record's own target URI, so it is free to emit.

No `WARC-Refers-To-Date` alongside it. The cache persists no capture timestamp: the field list in `cache_add()` ends at `Last-Modified`, which is a document property, and the zip entry's own date is set from that same value. Emitting it would assert that a record exists with that `WARC-Date`, which is false and would misdirect pywb's `closest=` lookup. Both engines already handle the field being absent, pywb by falling back to the CDX timestamp and wabac.js to the revisit's own. A real date needs a capture time in the cache, which is worth doing when the segment work lands and a previous index is being read anyway.

The validator now requires every revisit to name a capture, and requires a server-not-modified one to name its own URI. Test 73 already drives it over an archive of revisits; against the pre-fix engine it fails there.

Stacked on #788. Without it the new header line is what pushes a 995 to 1004 byte URL past `wbuf_printf`'s old 1024-byte buffer, and `warc_emit` then drops the record whole; the review caught that before this was pushed. The merge is in the branch so the pair is what got tested.

Closes #778

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 12:50:52 +02:00
Xavier Roche
dcdcc8745c A URL of 1005 bytes or more is silently dropped from the WARC archive (#788)
Every WARC header line is built with `wbuf_printf()`, which formatted into a 1024-byte stack buffer and returned `-1` instead of growing. `"WARC-Target-URI: %s\r\n"` costs 19 fixed bytes, so the line failed once a URL reached 1005 bytes, the `-1` reached the `goto done` in `warc_emit()`, and the record was abandoned. The page still got mirrored, the crawl still exited 0, and nothing was logged, so the only symptom was a URL missing from the archive.

`wbuf` reallocs already, so oversized output now formats straight into it after a `wbuf_reserve()`, which is the growth half of `wbuf_add()` split out. Every field benefits, not only the two carrying URLs. The second pass is bounded against what was reserved rather than trusted: advancing `len` by a return value larger than the reservation would push `len` past `cap` and corrupt the bounds check of every later append.

`-#test=warc-longurl` sweeps 100 to 9000 bytes across the boundary. Against the old formatter it fails at exactly 1005 and up, with 1003 and 1004 passing. Each record carries a distinct payload because identical ones dedupe into revisits, which would otherwise hide the response records the test counts.

One caveat on the sanitizer evidence, since it is easy to over-read. The buffer grows by doubling, so a small off-by-one lands in allocation slack where ASan cannot see it; that is why the second-pass bound is a logic check rather than something left to the sanitizer. The ASan+UBSan run over the sweep is clean, but only after planting a deliberate overflow in `wbuf_reserve` to confirm the probe actually fires. It did not, at first: libtool silently drops `-fsanitize` from the shared-library link, which produced no binary at all and a "clean" result that meant nothing.

Worth knowing for the segment work: `warc_emit()` returning `-1` also sets `w->failed`, which suppresses the archive swap added in #777. Before this fix a single over-long URL would make an `--update` pass throw away its whole archive and keep the previous one.

Closes #785

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 12:49:36 +02:00
Xavier Roche
c65d1c43c7 An FTP re-fetch truncated the mirrored file before the transfer (#799)
An FTP re-fetch called `filecreate()` on the mirrored file before a byte of the transfer had arrived, so a read error, a timeout or a short body destroyed the previous copy. HTTP has moved the good copy aside to a `.bak` since the #77 follow-up and puts it back when the transfer fails; FTP never took that backup. Rather than give it a second copy of the idiom, the backup moves out of `back_wait()`'s direct-to-disk block into `back_refetch_backup()`, which the FTP transfer now calls too. `back_finalize()` already restores it, so FTP inherits that. The REST resume branch appends and needs no backup.

That alone was not enough. `back_cleanup_background()` swaps a ready slot to the on-disk table through `back_clear_entry()`, which unlinks `back->tmpfile`, and a failed re-fetch sits at `STATUS_READY` unfinalized until the parser picks it up. The new `.bak` was being deleted in that window before `back_finalize()` could restore it, about half the time on a loaded box. `slot_can_be_cached_on_disk()` now refuses a slot that still owns a temporary, which closes the same window on the HTTP `.bak` and on the content-coding spool. The crawl-level race needs concurrency the suite cannot pin down, so `-#test=backswap` covers the predicate directly.

The suite had no FTP server. `tests/ftp-server.py` is a minimal one (PASV, SIZE, REST, RETR, LIST) with a mode file the test rewrites between passes, so a path can start failing without the port moving and taking the mirror directory name with it. Test 102 mirrors three files, then re-fetches one cut short, one served empty and one healthy: the first two must come back byte identical and be reported `unchanged`, the third replaced. It runs at `-c1` because a parallel FTP crawl loses whole transfers to #797, which is older and unrelated and would flake it on a loaded runner. #798 came out of the same work: an FTP `--update` sends `REST` over a complete mirror and splices the old file into the new body.

Closes #771

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 12:29:55 +02:00
Xavier Roche
95f8ebaa44 hts_rename_over() deletes its destination when the source is missing (#784)
The unlink-then-rename fallback in `hts_rename_over()` is there because Windows' rename() refuses an existing target, but it fired on any failed rename, ENOENT on the source included. A caller moving a temp file it never managed to write lost the destination and got `HTS_FALSE` back, which reads as "nothing happened". #754 unified four hand-rolled copies into this helper, so every call site inherited it.

The unlink now runs only for EEXIST, and only with a source that exists. EEXIST is the value that matters: the CRT maps ERROR_ALREADY_EXISTS there and keeps EACCES for a source another process holds, so accepting EACCES as well would have deleted the destination for a failure it had no part in, and the retry would then fail with the destination already gone. The source check is belt and braces for a CRT that reports neither. `hts_rename_utf8()` preserves errno across its free() calls now, since the gate reads it.

The existing call sites all derive their source's existence from a create that had to succeed first, so the bug was latent rather than live, but three of those destinations are user data: a mirrored file, a finished .wacz, and a rewritten page nothing will re-fetch. What is left of the window after this is #790.

The selftest probes what rename() does to an existing target and asserts against the regime it finds, so it runs three ways on Linux: native, then under an LD_PRELOAD rename() with Windows' shape, then under one reporting a locked source. The harness pins the expected regime per platform, so no leg can pass having exercised the other half. Seven mutants of the gate were each confirmed to fail it.

Closes #779

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 12:07:12 +02:00
Xavier Roche
3a096c589a x[strlen(x) - 1] indexes before the buffer on an empty string (#780)
Three bugs have already come out of the same one-liner: `x[strlen(x) - 1]` indexes one byte before the buffer when the string turns out to be empty (#729, #730, #768). Rather than wait for a fourth, this replaces the idiom everywhere with helpers that cannot underflow.

All 48 occurrences were reclassified by tracing each guard to where it actually lives instead of to the two lines above the index. 44 were already guarded, often a dozen lines up or in the caller, and one sits inside a commented-out block. The rest have no guard at the site, and two of those are reachable from a crawl.

`htscore.c:2194` is the one that matters: a one-byte stack out-of-bounds write in the end-of-mirror purge, confirmed under ASan and UBSan. `linput()` reads `old.lst` in 999-byte chunks, so a 1000-byte line comes back as 999 bytes plus a one-byte tail; for that tail `line + 1` is empty, and without `-O` so is `path_html`, which leaves `file` empty when the index runs. The crawled site controls the save-path length and therefore the line length. It needs a second run over the same project, which is what a re-crawl does. As controls, a 900-byte path and the same path with `-O` both stay clean.

`htsparse.c:2009` is a one-byte overread in the HTML parser, reachable from `<a href="   ">`, where the guard sits on the wrong side of the `&&`. The write on the next line is accidentally safe for the same reason. #768 itself turned out not to be reachable from a crawl, only through the `-#test=savename` hook, so I would not call that one a security fix.

The helpers live in `htssafe.h` beside the other bounded string operations, so no file needed a new include. Guards doing more than an emptiness check are kept, since folding those in would append a separator to an empty string.

`-#test=lastchar` checks the helpers against a poisoned neighbouring byte, including the `/`-before-the-buffer case from #768, and putting the missing length check back makes it fail. It also greps the source, so reverting any converted site fails instead of leaving the self-test green. `97_local-purge-longpath` drives the purge site end to end; on unfixed code the sanitized CI leg aborts at `htscore.c:2194`.

Built from identical source paths, 61 of 72 objects are byte-identical, one differs only in debug info, and the remaining 10 are the files edited.

The pointer spelling `x + strlen(x) - 1` has 25 occurrences and is filed separately as #781.

Closes #770
Closes #768

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 11:44:43 +02:00
Xavier Roche
1a4a5082b7 The --errpage placeholder block has been unreachable since 3.20.2 (#783)
The stand-in body `httpmirror()` builds for an error page has been unreachable since the 3.20.2 import: both guards say the opposite of their comments, so the block runs only when there is no save name and the URL *is* `/robots.txt`. `create_html_warning` is never assigned, so the HTML arm is dead outright. The GIF arm fires only if the user maps `.txt` to `image/gif` with `--assume`, and it then swaps `r.adr` for a 1070-byte buffer while leaving `r.size` at the error body's length, so the `robots_parse()` call below over-reads the heap.

Repair is not the one-character fix the inverted guards suggest. `filesave()` below writes `r.size` bytes, so uninverting the guards without also setting `r.size` moves the same over-read into the user's mirror. A correct repair would then replace every server error body with HTTrack's 2003 template by default, which is what `23_local-errpage.test` asserts is kept. Nobody has asked for the placeholder in twenty years, and #17 asked for the opposite.

`--errpage` itself is untouched: `store_errpage` keeps the server's error body and the normal save writes it. A byte-level differential against a master build over five error shapes under both `-o1` and `-o0` gives identical mirrors, and rebuilt objects differ only in `htscore.o`. The new test drives the one input that reached the block, and fails on master in a plain build rather than only under the sanitizers. Separately, the `-o` help text promises a generated page the engine has never produced; that predates this change and is filed as #787.

Closes #769

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 11:31:47 +02:00
Xavier Roche
4fd6767a8f A stale .bak silently disables the re-fetch backup on Windows (#776)
`back_finalize()` moves an existing file aside to `<file>.bak` before truncating it, so an aborted re-fetch can put the previous copy back. That rename was a bare `RENAME`, and Windows refuses to rename onto an existing target, so a `.bak` outliving a killed run made every later re-fetch of that URL fail the rename, take the `tmpfile = NULL` branch with nothing logged, and truncate the live file with no backup at all. The #77 guard was off for that file, silently and for good. `hts_rename_over()` (#754) unlinks and retries, which fixes it.

Worth a look, because clobbering a stale `.bak` is not free. A run killed between the rename-aside and the finalize leaves the previous complete copy in `.bak` and a partial in the live file, and the next re-fetch now overwrites the good one. It still looks like the right trade: POSIX `rename()` has behaved exactly this way since #77 landed, so the alternative is leaving Windows with a guard that stays dead until someone deletes the file by hand, and a leftover `.bak` is engine garbage that nothing advertises or ever restores. The clobber is logged now, so it is at least visible.

Any failure to create the backup is logged too, instead of quietly disabling the safety net. `htscache.c`'s static `hts_rename()` wrapper and the hand-rolled `old.zip` unlink at its only call site go the same way, which removes a copy of the unlink-then-rename idiom rather than adding a fifth.

Test 101 plants both kinds of leftover before the update pass: a stale file, which has to be clobbered so the `-M` abort can still restore the pass-1 copy, and a directory, which cannot be clobbered and has to be reported instead. Only the Windows leg arms the first half, since POSIX clobbers on its own; the second is armed everywhere. Test 37 gains a live update pass so the dead pass rotates onto an existing `old.zip`, which nothing covered.

Two older bugs on the same path came out of the review and are filed separately: #774 (the `.bak` name collides with a mirrored file of the same name, reproduced) and #775 (a failed `filecreate()` on a chunked re-fetch commits the backup away).

Closes #758

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 11:10:17 +02:00
Xavier Roche
56389103bf An --update pass overwrites the previous WARC and leaves a page-less WACZ (#777)
* warc: keep the previous archive when a pass has no bodies to replace it

A second crawl into the same output reopened the WARC with "wb" and
truncated it. On a cache-served pass nearly every URL comes back 304, so
the new file held revisit records whose payloads had just been deleted,
and the regenerated WACZ came out with zero page rows: a package that
replays nothing, in place of one that replayed fine.

The writer now builds into a sibling .tmp whenever an archive is already
there, and only swaps it in at close if the result can stand on its own.
A pass that only revisited URLs it did not re-download keeps the previous
.warc.gz, .cdx and .wacz untouched and says so.

What --update should ultimately mean for WARC output is still open; this
only stops the silent destruction in the meantime.

Closes #759

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

* warc: guard the segment swap and cover the rotated archive

A run that lost a record or a segment must not replace a whole archive,
and hts_rename_over unlinks its destination when the source is missing,
so every segment has to be on disk before the first rename.

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

* warc: force rotation in the segment test and guard its vacuity

--warc-max-size 2000 never rotated under the harness's --robots=0, so the
segment test was checking a single-file archive; a mutant that renamed
only segment 0 passed it. 600 rotates, and --archive-min-files fails the
test if a shrinking crawl ever stops producing the segments it checks.

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 10:24:06 +02:00
Xavier Roche
a4db58f7b7 hts_finish_html_file is documented as skipping unchanged writes, and does not (#764)
The MD5 comparison the comment promises left the tree in two steps: #467
extracted the function out of htsparse.c's HT_ADD_END macro without the skip
branch, and #512 removed the //[HTML-MD5]// cache entry it read. Drop the
parenthetical; the write is unconditional.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 10:11:11 +02:00
Xavier Roche
1f9728d816 --purge-old deleted a file whose re-fetch never got a response (#765)
* A failed re-fetch overwrote the mirrored file with the aborted read's debris

A transfer that dies before a complete response has no body, yet the save path
still consulted r.adr, which at that point holds whatever the aborted header
read left behind: raw status-line bytes, or an empty buffer that truncated the
file to zero on macOS. Require a successful transfer, as the empty-body half of
the condition already did.

Closes #748

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

* tests: assert the failed re-fetch keeps its bytes on every platform

Test 93 filtered reset.bin out of its bucket lists because a connection killed
before the status line surfaced differently per platform. It no longer does, so
assert the resource like any other: unchanged, with the bytes pass 1 mirrored.

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

* tests: make the re-fetch assertions catch a crawl that never re-fetched

The new test passed with no second pass at all, so a regression that stopped
re-fetching would have looked green. Assert the failure the fixture provokes,
give stay.bin a fresh pass-2 body so a fix that stopped overwriting anything
fails, and compare reset.bin by checksum rather than by length.

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

* --purge-old deleted a file whose re-fetch never got a response

A transfer that dies on the wire leaves the previously mirrored copy in place,
but back_finalize() returned without noting it, so the URL fell out of new.lst
and the end-of-update purge treated the file like a page that had vanished from
the site. Note the surviving copy, the way the incomplete-transfer branch above
already does, for the connection-level failures htsparse.c retries on. A
deliberate skip (too big, MIME-excluded, cancelled) keeps its current fate.

Closes #746

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

* Keep the copy for every failure, not just the five retryable codes

A malformed status line, an oversized declared length or a mid-flight abort all
land on STATUSCODE_INVALID, outside the retryable set, and the purge still ate
the file. Invert the test: any failure keeps the copy except the codes that
mean the engine passed the resource over on purpose.

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

* tests: assert the retry exhaustion, not the per-platform failure message

The message a cut connection produces depends on whether any bytes arrived, so
matching it would fail on a runner that sees 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-07-27 10:03:31 +02:00
Xavier Roche
99bd9bb674 A failed re-fetch overwrote the mirrored file with the aborted read's debris (#763)
* A failed re-fetch overwrote the mirrored file with the aborted read's debris

A transfer that dies before a complete response has no body, yet the save path
still consulted r.adr, which at that point holds whatever the aborted header
read left behind: raw status-line bytes, or an empty buffer that truncated the
file to zero on macOS. Require a successful transfer, as the empty-body half of
the condition already did.

Closes #748

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

* tests: assert the failed re-fetch keeps its bytes on every platform

Test 93 filtered reset.bin out of its bucket lists because a connection killed
before the status line surfaced differently per platform. It no longer does, so
assert the resource like any other: unchanged, with the bytes pass 1 mirrored.

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

* tests: make the re-fetch assertions catch a crawl that never re-fetched

The new test passed with no second pass at all, so a regression that stopped
re-fetching would have looked green. Assert the failure the fixture provokes,
give stay.bin a fresh pass-2 body so a fix that stopped overwriting anything
fails, and compare reset.bin by checksum rather than by length.

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

* tests: assert the retry exhaustion, not the per-platform failure message

The message a cut connection produces depends on whether any bytes arrived, so
matching it would fail on a runner that sees 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-07-27 09:44:28 +02:00
Xavier Roche
29dfd2df59 htsserver never returns from main(), it blocks on its own exit wait (#757)
htsthread_wait_n(background_threads - 1) subtracts one more than the count of
threads that must not be joined. Without --ppid that count is zero, so the wait
asks for a negative number of outstanding threads and the counter never gets
there; with --ppid it waits on the pinger, which by design never returns.

Wait for background_threads instead, which is what the sibling call inside
back_launch_cmd() already passes.

Closes #753

Signed-off-by: Xavier Roche <xroche@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 09:43:51 +02:00
Xavier Roche
55765815f5 htsAddLink walks back from an empty codebase, one byte before the buffer (#767)
* htsAddLink walks back from an empty codebase, one byte before the buffer

Same idiom as the lienrelatif() underflow fixed in #729: the trim that walks
back to the last '/' starts at codebase + strlen(codebase) - 1, which is
codebase - 1 when the string is empty, and the loop dereferences it before
a > codebase stops it.

Unlike #729 there is no reachable empty value. codebase is copied from a
recorded link's fil, and no hts_record_link() call site can supply an empty
one: every fil is either a literal seed or an ident_url_absolute() /
ident_url_relatif() success return, and fil_simplifie() restores "/" or "./"
rather than leaving a path empty. The guard goes in anyway, and -#test=addlink
drives the walk directly: under the sanitize job's ASan+UBSan build the test
fails on the unfixed walk and passes with the guard. Its second case pins the
ordinary trim, which the guard leaves alone.

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

* review: add the case that actually notices the codebase trim

For an ordinary relative link ident_url_relatif() re-derives the directory
from the path it is handed, so deleting the trim outright left the two
existing cases green. A query-only link ("?x=1") copies that path whole, and
does catch it.

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 09:40:11 +02:00
Xavier Roche
869b8479e9 structcheck() builds its rename target with an unbounded sprintf (#762)
* structcheck() builds its rename target with an unbounded sprintf

Both structcheck() and structcheck_utf8() move a regular file sitting where a
directory belongs, and build the "<name>.txt" target with a raw sprintf into a
2048-byte buffer. It stays in bounds only because of a strlen(path) >
HTS_URLMAXSIZE guard dozens of lines above, which nothing at the write site
mentions. Route both through sprintfbuff() and fail with ENAMETOOLONG, so the
bound is local.

Armed the probe: with that distant guard patched out, a 2045-byte path makes
the old sprintf write 2049 bytes into tmpbuf[2048] under ASan; the same build
with sprintfbuff() returns -1 and reports nothing.

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

* review: tighten the structcheck self-test and its comments

The path builder could end a path with a bare separator when the base
directory's length hit the wrong residue, so the test aborted on a long
$TMPDIR. The refusal case also asserted on a component structcheck never
creates; assert on the outermost one instead.

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

* tests: drop the max-length rename case, macOS PATH_MAX is 1024

The path the guard admits (HTS_URLMAXSIZE) plus ".txt" is longer than macOS
accepts, so fopen() failed there. The case could not tell a fixed build from an
unfixed one anyway; what is left covers the guard and the rename on both entry
points.

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 09:34:02 +02:00
Xavier Roche
9fe47c3986 Remove dead HTS_USEZLIB guards now that zlib is mandatory (#761)
configure has rejected --without-zlib since #750, and htsglobal.h
#errors on a forced HTS_USEZLIB=0, so the macro can only ever be 1.
Collapse every #if/#ifdef HTS_USEZLIB guard (htsweb.c, htsname.c,
htslib.c, htszlib.c, htsselftest.c, htscodec.c) to its always-taken
branch; htsweb.c's guard used #ifdef where every other site used #if,
an inconsistency that no longer matters once the guard is gone.

htscodec.c's #else arms were a genuine zlib-free content-coding path
(Accept-Encoding: identity, hts_codec_unpack returning -1), not stubs.
Removed for consistency with the other nine guards: the cache
(htscache.c, proxy/store.c) already reaches minizip unconditionally
from ~60 call sites with no null backend, so a zlib-free build is not
actually reachable today regardless of this file.

No new test: this is dead-code removal with no behavior change.
Verified by differential build against master: htscodec.o and
htszlib.o are byte-identical; the other touched objects differ only
in __LINE__ immediates shifted by the removed guard lines (plus one
cosmetic objdump label-annotation artifact each in htsselftest.o and
htsweb.o, from string-literal pool reordering). Exported symbols in
libhttrack.so are unchanged. make check: 166/166 (158 pass, 8 skip).

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 09:05:52 +02:00
Xavier Roche
37fa549ac5 Add the regression test #747 could not carry when it landed (#760)
htsselftest.c and tests/Makefile.am were held by #718 while #747 was fixed, so
the thread-counting fix went in without a test. -#test=threadwait covers it
from both sides: a wait placed right after a spawn joins that thread, and
wait_n(n) leaves n running rather than draining them.

One spawn per round is what makes it bite. A batch gives the earlier threads
time to raise the counter themselves, which is why an eight-thread version
passed on the unfixed engine; one thread per round failed 10 runs out of 10.

The changes-race self-test can now drop the counter it kept because
htsthread_wait() could not be trusted to join.

Signed-off-by: Xavier Roche <xroche@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 09:05:44 +02:00
Xavier Roche
de8c0eebfc Three hand-rolled copies of the unlink-then-rename fallback, with two return conventions (#754)
* One unlink-then-rename helper for the three copies

replace_file() (htsback.c), wacz_rename_over() (htswarc.c) and the inline
block in singlefile_rewrite_file() each worked around Windows' non-clobbering
rename, with two opposite return conventions and only one of the three
converting path separators. Copying the wrong one gets you an inverted success
test.

hts_rename_over() replaces all four call sites: hts_boolean return, fconv() on
both paths. It lands in htsname.c rather than the htstools.c the issue named,
to stay clear of an in-flight PR over that file.

The wacz test now runs a cacheless second pass over a poisoned copy of the
package the first pass wrote, so repackaging has to clobber an existing file.
43 and 74 also assert the failure warnings stay out of the log, which is all
an inverted return leaves behind at those two sites.

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

* rename helper: move hts_rename_over to htstools.c

Issue #726 asked for htstools.c; it went to htsname.c only because #718 held
the file at the time. Kept internal, not HTSEXT_API: htscore.h pulls in both
headers, so every call site reaches it either way.

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

* format: drop the trailing blank line left by the move

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

---------

Signed-off-by: Xavier Roche <xroche@gmail.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 08:58:20 +02:00
Xavier Roche
9e29c1e159 Sitemap files are never read, so URLs nothing links to are never found (#718)
* Read sitemap files so URLs nothing links to are found

HTTrack finds URLs only by parsing links, so anything a site publishes solely
in its sitemap stayed invisible: robots.txt was already parsed, but its
Sitemap: lines were ignored and nothing else in the tree touched sitemaps.

Adds opt-in --sitemap (-%m), which probes the start host's robots.txt and
falls back to /sitemap.xml, and --sitemap-url (-%mu) for an explicit document.
Handles <urlset> and nested <sitemapindex>, plain or gzipped. Discovered URLs
enter with the full depth budget but still go through the wizard, so filters
and scope rules decide; a sitemap is not a filter bypass.

The parser reads attacker-controlled XML off the network, so it is capped on
URL count, index nesting, decompressed size and decompression ratio, and child
sitemaps must stay on the host that named them.

Closes #712

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

* sitemap: distinguish the sitemapindex log line from a urlset one

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

* sitemap: fix an out-of-bounds read, tighten the parser and the tests

lienrelatif() walked back from the last character of its current-path
argument without checking the path was non-empty, reading one byte before
the stack buffer. htsAddLink is the first caller to pass an empty savename,
which sitemap documents have because they are ingested rather than mirrored,
so ASan caught it on the new crawl test.

The parser drops a value whose numeric character reference decodes outside
printable ASCII, rather than leaving the reference verbatim and seeding a URL
the site never published, and classifies a document by its real root element,
so a comment naming the other one no longer flips urlset and sitemapindex.
The robots.txt line reader is bounded by the body size instead of relying on
a NUL terminator.

The self-test moves to 01_zlib-sitemap.test: MSan runs 01_engine-* only,
because an uninstrumented libz floods it with false positives.

Tests gain the assertions the earlier ones were missing: which of the
robots.txt route and the /sitemap.xml fallback was taken, that the sitemap
documents stay out of the mirror, that the off-host child sitemap is refused,
the sitemapindex nesting cap, the per-document URL cap at its production
value, and copy_htsopt coverage for the two new fields.

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

* sitemap: state what the decompression cap actually binds on

deflate tops out near 1032:1, so hts_codec_maxout never binds before the
64 MiB cap; the old comment implied a ratio guard that cannot fire.

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

* sitemap: a child sitemap is a fetch, so filters and robots.txt must gate it

An adversarial review found that a <sitemapindex> <loc>, and a robots.txt
Sitemap: line, went straight to hts_record_link: the request went out even
when a -* rule or robots.txt Disallow covered it. Only the <urlset> half ran
through the wizard. Gate the document itself on the filters and on
robots.txt, which is all that can apply: the wizard proper wants a referring
link, and its up/down travel rules would judge a child sitemap against the
parent sitemap's own directory. The robots.txt probe is exempt, being the
request that fetches the rules.

A 301 also used to end ingestion silently, since the engine re-queues the
target as a fresh link that carried no sitemap marking. That hit any site
redirecting http to https. The marking now follows the redirect.

The "N URL(s) added" counter reported what the scanner emitted rather than
what was taken, which hid both of the above; it now reads "N of M". The
fallback to /sitemap.xml keys on the same corrected count, so a robots.txt
whose only Sitemap: line is off-host or filtered still falls back. Root
classification skips a UTF-8 BOM and an XML namespace prefix, and the doc
list is cleared when a mirror starts rather than only when it ends.

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

* sitemap: gate each fetch by who asked for it, and anchor travel on the start URL

A nine-agent review found a scope escape: the sitemap document was its own
`premier`, so the wizard measured travel from wherever the site chose to put
its sitemap. A root /sitemap.xml therefore widened a /deep/dir/ crawl to the
whole host. The ingester now points the wizard at the crawl's own start link
and lets each seeded URL become its own anchor, which is what a command-line
seed gets.

Robots handling was both mistimed and undifferentiated. The Sitemap: lines are
now collected by robots_parse, on the same body in the same fetch, and acted on
after the parsed rules are installed rather than before; and the decision comes
from a new hts_robots_forbids extracted out of the wizard, so the sitemap path
inherits the -s1 filters-win override instead of a stricter hand-rolled check.
The four fetches are no longer treated alike: a sitemap the user names is user
intent, one the site declares invites the fetch, only the guessed /sitemap.xml
obeys a Disallow, and the URLs listed inside stay fully gated.

Also: hts_unescapeEntities replaces the private entity decoder, whose guard
tests and fuzz corpus it silently forfeited; hts_codec_head replaces hts_zhead,
which is only defined under HTS_USEZLIB; the composed URL buffer now fits two
maximal components plus a scheme, which a 2046-byte --sitemap-url reached; the
bounded search is promoted to htstools as hts_memstr; and the live state moves
from httrackp into htsoptstate, leaving two installed fields rather than three.

Tests gain the scope escape, the three robots cases, a cap-boundary control,
the handler invocation count and a compression-bomb decode. Every one was
checked against a deliberately broken build.

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

* sitemap: add the fuzz harness the parser was missing, and drop truncated Sitemap: lines

The file header called the scanner fuzzable while fuzz/ registered ten
harnesses and none for it. fuzz-sitemap feeds it raw XML, gzip-framed bodies
and truncated streams off a heap copy of exactly the input size, so an overread
is an ASan report rather than a quiet pass, with a four-file seed corpus.
60000 runs clean under ASan+UBSan.

robots_parse now drops a Sitemap: line that filled its scratch buffer instead
of handing on the half URL it was truncated to.

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

* fuzz: keep only the four sitemap seed inputs

A libFuzzer run writes its finds into the first corpus directory, and 191 of
them were committed with the harness.

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

* selftest: bound the sitemap document builders' snprintf accumulation

snprintf returns the length it wanted to write, so accumulating it blind
lets the next offset and size argument walk past the buffer. Guard each
step the way the argv builder above already does, and give the per-URL
loop a real remaining-space bound instead of a fixed 33.

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

* sitemap: date the new files 2026

The headers were copied from an existing file and kept its 1998 year.

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

* sitemap: keep the ingestion state out of htsoptstate

htsoptstate is embedded by value as httrackp.state, so a field at its tail
shifts every httrackp member declared after it: an offsetof probe put
warc_file at 141752 on master and 141760 on the branch. Move the pointer to
httrackp's own tail, where every existing offset holds and copy_htsopt still
ignores it.

Also renumber the crawl test to 89, master having taken 87 and 90, and give
the new option8 checkbox the hidden companion that 90_webhttrack-checkbox-clear
requires, plus its row in that test's table.

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

* tests: renumber the sitemap crawl test to 95

Master took 88 through 93 and #720 claims 94.

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

* sitemap: realign the htsopt.h comments after the single-file merge

Master's longer LLint declarator moved the block's comment column.

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

* sitemap: translate the new GUI strings into the remaining 28 locales (#738)

The feature PR added the four LANG_SITEMAP* entries to lang.def with English
and Francais only; every other locale fell back to English in the WebHTTrack
form. Each file is written in its own declared charset.

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Signed-off-by: Xavier Roche <xroche@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 07:59:59 +02:00
Xavier Roche
9571fb9a6a htsthread_wait() returned before threads it should join had started (#752)
process_chain was incremented by the child in hts_entry_point(), so a caller
that spawned threads and immediately called htsthread_wait() saw a zero count
and returned at once, free to tear down state the children were about to read.

Count at spawn instead, under the same mutex the waiter reads.

httrack.c freed the option block before waiting; wait first.

Closes #747

Signed-off-by: Xavier Roche <xroche@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 07:57:47 +02:00
Xavier Roche
3e8595c46f zlib is mandatory: reject --without-zlib at configure instead of failing at link (#750)
--without-zlib only ever dropped -lz from LIBS; it never defined HTS_USEZLIB 0,
so every #if HTS_USEZLIB guard in the tree has been permanently true and the
build died with undefined references from minizip, htszlib.c, htswarc.c and
htsselftest.c. A zlib-free build is not reachable from there: the cache and the
WARC output are zip/gzip containers, and htsback.c already #error'd on
HTS_USEZLIB=0.

So make the requirement explicit. CHECK_ZLIB now errors out on --without-zlib
and on a missing header or library, keeping --with-zlib=DIR for a non-standard
prefix. htsback.c's #error moves to htsglobal.h where the knob is defined, with
a message that is accurate when it fires; its include of htszlib.h went with it,
unused.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 07:55:59 +02:00
Xavier Roche
0774d47d2f No single-file HTML output with inlined assets (#720)
* Single-file output is MHT, which browsers no longer open

Add --single-file (-%Z): after the mirror completes, rewrite every saved
page in place with its stylesheets, scripts, images and fonts embedded as
data: URIs, while links between pages stay relative. The mirror remains a
browsable tree and each page also stands alone.

This cannot reuse the -%M path: MHT streams a MIME part per file as it is
saved, but a data: URI needs the asset's bytes when the page is written,
and pages are normally saved before their assets are fetched. The new pass
runs over the finished tree at the tail of httpmirror(), after the update
purge.

Audio, video, page-to-page links and anything over --single-file-max-size
(10 MB default) keep an ordinary link. References carrying a scheme are
skipped, which covers data: and makes a second --update run a no-op.
Resolution is clamped to the mirror root: the HTML is hostile input.

htsopt.h gains two tail-appended fields; VERSION_INFO is untouched.

Closes #713

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

* tests: renumber the single-file test to 87, run the self-test last

Master took 82 through 85 while this branch was open. The engine self-test
also moves to the end of the script: it and the crawl assertions cover
different ground, and failing first hid the crawl half.

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

* Fix the parser and path bugs review found, and cover them

sf_relative_from() indexed one byte past from_dir's terminator whenever the
page's own directory was a prefix of the asset path, which is the ordinary
mirror layout: an out-of-bounds read that also miscounted the ../ prefix and
emitted a broken link. Two review agents hit the same ASan trace; the branch
had no test because every nested asset in the fixture was under the cap.

Also from review: an escaped quote no longer ends a CSS string early (which
exposed its contents to the url()/@import scanner), @import url(...) now
inlines like the quoted form, a raw-text element ends only on a real end tag
rather than any prefix of one, an over-wide tag is copied through by the same
quote-aware scan instead of a second quote-blind one, <!--> is an empty
comment, whitespace before a tag's > survives, and a page cannot inline more
than SINGLEFILE_MAX_PAGE_SIZE, which bounds the multiplicative @import
fan-out. A failed encode no longer leaves a payload-less data: prefix behind,
and base_dir is matched against the root on a component boundary.

sf_readfile now wraps a new readfile2_utf8() rather than being a fifth copy
of the readfile family. Docs place --single-file beside -%M instead of
implying it supersedes it: MHT stays the better container, this wins on
opening anywhere.

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

* singlefile: tighten two comments

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

* tests: cover the option plumbing review found untested

copy_htsopt's two new fields, including the >0 guard that must not let an
unset source clear the target's default; -%Z0; and a rejected
--single-file-max-size argument falling back to the built-in cap.

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

* tests: stand the single-file GUI check down where htsserver is absent

The Windows job builds httrack.exe only, and reaches this file through its
*_local-*.test glob, so requiring htsserver failed the whole test there even
though every crawl assertion had passed. Skip that half instead, and run the
engine self-test before it so the skip cannot swallow it.

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

* tests: check the sprintfbuff master just made warn_unused_result

#722 gave slprintfbuff the attribute, so the over-wide-tag fixture's call
became the one warning this branch adds over master's baseline. Handle it the
way that PR's own self-test code does; the buffer cannot truncate here.

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

* single-file: translate the new GUI strings into the remaining 28 locales (#739)

The feature PR added the four LANG_SINGLEFILE* entries to lang.def with English
and Francais only; every other locale fell back to English in the WebHTTrack
form. Each file is written in its own declared charset.

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

* single-file: restore the mirror file mode, and give the guards real coverage

The rewrite spools to a .sfnew temp and renames it over the page, which
bypasses filecreate() and with it the HTS_ACCESS_FILE chmod the engine puts on
every other mirrored file. Under a restrictive umask the pages came out 0600
while their assets stayed 0644, so a mirror served by a webserver or shared
with a group lost read access on exactly the pages. chmod the spool before the
rename.

Two guards had no coverage: deleting the scheme/data: check in sf_resolve left
both the self-test and the crawl test passing, because the fixtures resolved to
paths that were absent either way, and the per-page inline budget was never
exercised. The fixtures now plant a file where each guard's removal would land
the walk, and a self-importing stylesheet measures the budget against a
large-budget control. Charging that budget after the nested rewrite instead of
before let an @import chain spend what its ancestors had already claimed and
drove it negative; charge it up front and refund on failure.

The attribute table missed the lazy-loading attributes hts_detect[] already
downloads, so a modern page inlined almost nothing: add data-src, data-srcset,
lowsrc, object@data and embed@src, and record why the rest stay links.

The new web GUI checkbox had no hidden companion input, so it could be ticked
but never cleared, which is what #725 fixed for every other box. Master's test
90 catches it once the branch merges.

Adds a libFuzzer harness over the rewriter, since it re-serializes hostile
HTML, and renames the crawl test to 91 now that master holds 87 and 90.

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

* single-file: document the inlined-stylesheet limitation in the CLI guide

Recorded in htssinglefile.h already; the user-facing guide is where someone
raising --single-file-max-size will look.

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

* tests: move the Windows note down to the gate it explains

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

* ci: register the single-file test's Windows skip

Its GUI half needs htsserver, which the Windows job does not build, so the test
now exits 77 there instead of reporting a pass for assertions it never ran. The
skip list is pinned, so it has to be declared. Both Windows jobs reported
fail=0; only the list check was red.

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

---------

Signed-off-by: Xavier Roche <xroche@gmail.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 07:17:13 +02:00
Xavier Roche
7e2209d4cc No way to see what changed between two mirrors (#721)
* Report what a crawl changed against the previous mirror (--changes)

--update already knows which resources were new, which changed and which the
server called unchanged, and throws it away: the flags reach file_notify() and
go no further than a log line, while deletions exist only as a side effect of
purging. --changes (-%d) keeps all of it and writes hts-changes.json plus a
one-line summary in the log.

"Changed" means the bytes differ, not that the server re-sent the resource.
Comparing the mirrored files directly would not work: HTTrack stamps every
parsed page with the crawl date via the footer, so those bytes differ on every
run. Payloads are compared instead, the previous one coming from the cache for
parsed pages and from the local copy sampled just before it is overwritten for
everything else.

The mirror-relative path, not the URL, is the accumulator's key, so a redirect
and its target that share a save name are one entry; and only the first notify
for a file samples its pre-run state, so a retried transfer is not counted
twice. What counts as already mirrored comes from the previous run's file
index rather than from the file's presence on disk: a partial left by this
crawl's own failed attempt is on disk but was never part of the previous
mirror.

The deleted set is now computed whether or not purging is enabled; unlinking
still happens only under --purge-old.

Closes #714

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

* changes: never leave a stale report behind

A crawl that mirrored nothing created no accumulator, so hts_changes_close_opt
returned without writing and the previous run's report stayed on disk as if it
described this one. Write it whenever --changes is on. The no-data rollback is
the deliberate exception, and is now documented: it restores the previous cache
generation, so leaving the matching report alone is the consistent behaviour.

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

* changes: drop em dashes from the format page

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

* changes: fix the review's three findings

The size shortcut compared rendered on-disk sizes even for parsed pages, whose
payload digests describe something else entirely, so it decided the outcome
before the payload comparison could run: a page whose payload never changed but
whose rewritten links moved read as changed. It now only applies when both
digests describe the file on disk.

file_notify() reaches the accumulator from the FTP download thread as well as
the main one, and the lazy allocation, the coucal write and the entries realloc
were all unguarded. Every entry point now takes a mutex, and the HTML hook does
its cache read before taking it, since that read can itself re-enter
file_notify() and move the array.

Two fixtures cover what nothing did: a gzipped direct-to-disk body that changes
at constant length (without the pre-sample before the decoded temp is renamed,
it reads as unchanged), and a page with a fixed payload behind a redirect whose
target is renamed, so its file on disk changes length while its bytes do not.
Both were checked against builds with the respective fix reverted.

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

* changes: document the renamed-file case in the format page

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

* changes: refresh two stale test comments

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

* Merge origin/master into feat/change-report

Both sides appended to tests/Makefile.am's TESTS; kept master's
86_local-proxytrack-cache-longfields.test alongside 88_local-changes.test.

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

* changes: translate the new GUI strings into the remaining 28 locales (#740)

The feature PR added the two LANG_CHANGES* entries to lang.def with English and
Francais only; every other locale fell back to English in the WebHTTrack form.
Each file is written in its own declared charset.

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

* changes: fix the review's blocking findings

Lock the report path against the FTP thread the crawl never joins, seal the
accumulator once the report is written, key entries off the project directory
so the report survives --cache=0, stop calling a file gone when the crawl only
failed to re-fetch it, and skip the hook's work entirely when --changes is off.

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

* changes: prove the fixes, and give the report a cache-off mode

Adds a changes-race self-test (the FTP shape: notifier threads against the
report path), fixtures for a transfer the crawl never completes, for a leftover
file at a name the crawl mirrors fresh, and for a cache-off mirror, plus a pass
with purging on. Registers the web GUI's --changes box with the clearing
companion master's #725 now requires, and documents the degraded mode.

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

* changes: keep the failed-transfer case portable

A connection killed before the status line surfaces differently on macOS, where
it truncates the mirrored file to zero (#748). Assert only what holds on both:
it is never reported gone, and its file survives.

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

---------

Signed-off-by: Xavier Roche <xroche@gmail.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 06:58:33 +02:00
Xavier Roche
9484b32ecd An .ndx entry's two URL halves overflow the buffer they share (#744)
Both binput calls pass HTS_URLMAXSIZE, but they write into one
line[HTS_URLMAXSIZE * 2] and binput puts its NUL at s[max]. A first field
that fills its whole bound leaves the second writing line[2048], one past
the array. ASan reports a stack-buffer-overflow on a crafted .ndx. Bound
the second by what the first left.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 23:20:10 +02:00
Xavier Roche
1d647bfecd PT_GetTime's gmtime-failure fallback would print a day of 00 (#742)
* PT_GetTime's failure fallback formats as day 00

An all-zero struct tm has tm_mday == 0, and the ARC filedesc line prints
tm_mday raw, so a gmtime failure would emit "...0100" where the day
belongs. Use the epoch, as PT_SaveCache__Arc_Fun already does for an
unparseable Last-Modified.

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

* The fallback is reachable, so test it

I claimed no reachable trigger. Wrong: file_timestamp() passes st_mtime
through untouched, and a cache whose mtime is past gmtime's range takes
the fallback. Only the ARC loader is safe, because it overwrites the
timestamp with the filedesc line's own 4-digit-year date.

Test 88 sets an out-of-range mtime on a zip cache and pins the emitted
date; without the fix it reads 19000100000000.

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

* Register the Windows skip, and make the skip path real

The Windows job pins an exact expected-skip list, so a new conditional
skip fails it; test 88 skips there because NTFS will not hold an mtime
past gmtime's range. Its own skip logic was also dead code: "rc=0 || rc=$?"
never runs the right-hand side, and only set -e was carrying python's 77
through. Capture the status properly and tell a clamp apart from a real
failure.

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 23:19:51 +02:00
Xavier Roche
b2dc012263 Route ProxyTrack's remaining raw strcpy through the wrappers (#743)
Fourteen sites, all safe today: literals into sized fields, and two
same-sized array copies. The three writing "" through r->location, a
char *, become a direct terminator, since strcpybuff would have taken its
pointer path and lost the bound.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 22:59:43 +02:00
Xavier Roche
aa1131982b Three hand-written copies of the same clipping contract (#741)
* Fold the three copies of the clip idiom into strclipbuff()

htscache.c and the two readers in proxy/store.c each spelled out
clear-then-strlncatbuff, in binaries that share no code. A helper in
htssafe.h states the contract once, and evaluates its arguments once: the
macro form expanded refvalue and refvalue_size twice, and (refvalue_size)
- 1 would have wrapped to SIZE_MAX had any call site ever passed 0.

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

* Pin the capacity-2 boundary

The cases jumped from the degenerate capacity 1 straight to 8, so a
defect confined to small-but-not-degenerate sizes passed: clipping a
two-byte destination to the empty string instead of one character.

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 22:58:58 +02:00
Xavier Roche
f72e7ebe96 htsserver: an unauthenticated GET of a directory spins the server forever (#724)
* htsserver: a request naming a directory spins the server forever

fopen() succeeds on a directory on POSIX and every read from it fails with
EISDIR without ever raising EOF, so smallserver()'s "while (!feof(fp))" serving
loop never terminates. GET /server/ needs no session id and no project to reach
it, and the accept loop is single-threaded, so one unauthenticated local request
wedges WebHTTrack for good.

Refuse a directory before fopen() so the 404 branch answers, rather than only
ending the loop: a loop-only fix would serve every directory as an empty 200.
The two loops fed a client-influenced path also stop on ferror(), since a read
that fails for any other reason spins the same way.

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

* htsserver: whitelist regular files instead of blacklisting directories

fexist() (htsserver.h) is the stat + S_ISREG predicate this file already uses
for the "file-exists:" template op, so gate the serving fopen() on it rather
than on a fresh negated is_directory(). Refusing only directories still handed
FIFOs to the same code path, where fopen() blocks with no writer and kills the
single-threaded accept loop for good.

Test 91 gains the FIFO case and a POST that loads a project whose
hts-cache/winprofile.ini is a directory: that fopen() sits ahead of every guard,
so it covers the ferror() check on the project-load read loop.

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

* htsserver: fold the two comments above the serve guard into one

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 22:49:59 +02:00
Xavier Roche
3b53bf85f2 A posted project path overflows htsserver's "save settings" error messages (#723)
* htsserver: clip the "save settings" failure messages

The three sprintf(tmp[1024], ...) sites reporting a failed profile save
quote a project path composed from the posted "path" and "projname"
fields, neither of them bounded. A sid-authenticated save with a
1200-byte path smashes the stack buffer and takes the server down.

Route them through a SET_ERRORF() that formats into a fixed buffer and
absorbs the truncation once, so the message clips instead of aborting:
the text comes from the client, and a quieter denial of service is no
better than a loud one.

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

* tests: reach the second message without a 1030-byte mkdir

The tree the case needed came within a few bytes of macOS's PATH_MAX
once /var resolves to /private/var. A symlinked hts-cache gets there
instead: structcheck() lets a non-directory through, and opening
winprofile.ini under it fails with ENOTDIR whatever the uid.

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

* htssafe: one clipping printf helper for the three failf wrappers

htsblk_failf(), PT_Element_failf() and htsserver's format_error() all had
the same body; slprintfbuff_clip() now owns it. A (void) cast on
slprintfbuff() is no substitute: GCC warns through warn_unused_result.

vslprintfbuff() also empties dest before formatting, so a vsnprintf that
fails outright cannot leave the caller publishing uninitialized stack.

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

* tests: assert the clipped length, not just the message text

Two of the three sites only checked that the message appeared, which the
old sprintf did equally well; each now compares the rendered message
against the 1023-byte clip. The failing-write branch no longer needs
/dev/full either: the server runs under a file-size limit, so macOS gets
the same coverage.

Renumbered 86 to 89, which no other pending change claims.

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

* tests: build the oversized profile without a 16k printf width

The macOS leg reached the third message with no error set at all, so the
write the file-size limit was supposed to break had gone through. Build the
profile by doubling a 1024-wide printf, assert its length, and assert the
init file came out short, so a limit that does not bite names itself
instead of surfacing as a missing message.

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

* tests: build the long paths from the physical temp directory

That is what broke the macOS leg: TMPDIR there resolves through the /var
symlink, so the 1020-byte init file the third case opens was really 1028
bytes to the kernel and the open failed with ENAMETOOLONG. Both cases sit
within a few bytes of macOS's 1024-byte PATH_MAX, so the paths have to be
measured after resolution.

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 22:25:52 +02:00
Xavier Roche
ce7dcfa9de Options ticked on by default cannot be un-ticked in the web GUI (#725)
* Options ticked on by default cannot be un-ticked in the web GUI

An unchecked HTML checkbox posts nothing, so htsserver never overwrites the
value it already holds. Every box in the wizard is one-way: once the stored
value is "1", whether htsserver seeded it at startup, a loaded profile set it,
or the user ticked it earlier in the session, un-ticking and submitting leaves
the option on and draws the box ticked again. Only four boxes, all in
option1.html, carried the companion hidden field that guards against this.

Add it to every remaining bare checkbox, and switch cookies and parsejava to
${ztest:...} so a cleared box emits --cookies=0 / --parse-java=0; ${test:...}
renders nothing at all when the value is empty, which is not "off" for an
option the engine turns on by default.

index, urlhack and keep-alive are deliberately left alone: their long options
are declared "single" in htsalias.c and optalias_check drops the =value, so
--index=0 resolves to -I and turns the option back on. That parser bug is
pre-existing and needs its own fix.

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

* tests: pin last-write-wins and cover every checkbox

The runtime leg posted each name once, so a first-wins body parser would
have passed while the fix did nothing in a real browser: post the
duplicated name in both orders and assert the last value wins. Replace the
four hand-written option cases with a table covering all 28 non-skipped
boxes, asserting the command-line token and the Windows-profile key each
state emits, plus a completeness check so a new box cannot slip through
unexercised.

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

* tests: rename the loop variable shadowing the scanned page

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 22:08:46 +02:00
Xavier Roche
069573edc3 Test assertions read a padded or truncated reply as a clean security verdict (#728)
* tests: a failed request must not read as a clean security verdict

Under pipefail, "request | grep -q MARKER && fail" skips the fail when the
request itself errors: the leak checks in tests 78 and 85 then pass without
ever having run. Capture the reply first and fail loudly if it never arrived.

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

* AGENTS.md: record the fail-open assertion shape

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

* tests: a reply that proves nothing must not read as a clean verdict

The previous commit converted some of the fail-open assertions and left three.
78's refusal loop still piped into "grep -q ... && fail": grep -q exits on the
first match and SIGPIPEs the producer, so under pipefail a hostile reply that
pads its Location past the 64 KB pipe buffer suppresses the failure exactly as
a dead probe would. 85's fetch() only required a non-empty reply, so a
truncated body or a 302 to the file passed the leak checks marker-free, and no
assertion looked at the status line at all. 78's store probe had no emptiness
guard, so an empty page read as "the store was not written".

Match from here-strings throughout, give fetch() the status each caller
expects, and route 78's store probe through a helper that requires a served
page. 77's X-Injected check had the same shape.

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 22:08:10 +02:00
Xavier Roche
a75f437df9 lienrelatif() reads one byte before its stack buffer on an empty path (#729)
The trim that walks back to the last '/' starts at `curr + strlen(curr) - 1`,
which is `curr - 1` when the path is empty. The loop then dereferences it.

An empty path is reachable today: the pre-pass that strips a query does
`strncatbuff(newcurr_fil, curr_fil, a - curr_fil)`, so any `curr_fil` starting
with '?' hands the walk an empty string. `-#test=relative "dir/page.html" "?x"`
under ASan reports the underflow.

The read is one byte and the loop stops immediately either way, so the guard
changes no output: over the 484 ordered pairs of a 22-value path corpus, run
against builds that force the byte before the buffer to 0 and to '/', the
guarded and unguarded results are identical.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 22:06:11 +02:00
Xavier Roche
783f6ee1f5 AGENTS.md: record what the msg[80] hardening batch taught (#737)
Five PRs across the engine and ProxyTrack turned up the same few traps
more than once, and none of them are obvious from the code.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 21:39:57 +02:00
Xavier Roche
913caf68be Clear the last three compiler warnings (#733)
* Clear the last three compiler warnings

finalurl was sized for one of the two URLs it concatenates. The IIS-bug
example callback overwrites a suffix in place with a same-length
replacement and must not terminate the string, which is memcpy, not
strncpy. The coucal bench's if/else chain has no final else, so result was
only initialised on the paths gcc could not prove exhaustive.

A clean build now reports zero warnings.

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

* Bound the IIS suffix copy by what matched, not by the table

Copying strlen(replacement) leaves the "MUST be the same sizes" comment as
the only thing standing between a future table edit and an overflow. j is
the number of bytes just matched in the destination, so copying j is safe
whatever the table holds.

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 20:37:52 +02:00
Xavier Roche
59660102d6 A cache field wider than ours aborts the engine instead of clipping (#732)
* A cache field wider than ours aborts the engine instead of clipping

The read-side ZIP_READFIELD_STRING used strlcpybuff, and the whole *_safe_
family aborts on overflow rather than truncating. Since the header line is
bounded only by HTS_URLMAXSIZE and msg is 80 bytes, a cache written by
another build, or a corrupt one, kills the crawl outright. The corrupt-cache
self-test already promises "rejected per-entry, never crash".

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

* Pin the clip to each field's own capacity

Review found one case exercised only msg[80], so a hardcoded clip length
passed. lastmodified[64] is narrower, and no single constant satisfies
both. The forged replacement was also one byte longer than the line it
overwrote, which only worked because corrupt_patch copies exactly the
pattern length.

Also stop claiming another build's cache can trigger this: the writer
emits each field from the same struct the reader fills, so it takes a
corrupt cache.

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 20:37:38 +02:00
Xavier Roche
e96399910b Share the in-progress display struct instead of copying it (#734)
t_StatsBuffer was defined twice, byte for byte, in httrack.h and htsweb.h,
with NStatsBuffer duplicated alongside. httrack and htsserver each keep
their own array, so nothing catches the two drifting apart, and the last
change to the struct had to be applied to both by hand. Both now include
src/htsstats.h; sizes and offsets are unchanged.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 20:19:01 +02:00
Xavier Roche
52d0ab2356 An entry with no usable Last-Modified crashes proxytrack --convert (#731)
* Cached entry with no usable date crashes the ARC writer

PT_SaveCache__Arc_Fun dereferenced convert_time_rfc822() straight into the
record line, so any entry whose Last-Modified is absent or unparseable took
proxytrack --convert down. The sibling caller a thousand lines up already
guards the same call; this one fills in the epoch instead.

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

* Assert the archive date, not just the entry

Review found the test blind to the two mutants that matter: a guard firing
unconditionally clobbers every valid date to the epoch, and one that skips
the year and day emits a month and day of 00. Grepping only for the URL saw
neither. Assert the date field exactly, and add a valid-date case so the
untouched path is pinned too.

A bare "Last-Modified: 0" crashes the same way, so it joins the cases.

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 20:18:58 +02:00
Xavier Roche
ca533abefd Unbounded writes into the 80-byte htsblk failure message (#722)
* Bound the remaining writes into htsblk.msg[80]

Nine writers still filled the 80-byte msg with no bound. Six sprintf the
result of strerror(), whose longest glibc string is 49 bytes in the C
locale and 72 in fr_FR against 46 bytes of room after the longest prefix.
Realistic connect() errnos still fit, so this was latent rather than live.

The FTP helper's .ok result file had no excuse: it was copied byte by byte
until EOF into the same 80 bytes. Split that parse out as
back_read_ftp_result() so a self-test can drive it, and stop at capacity.

These sites never appeared in the -Wformat-truncation cluster that added
htsblk_failf: the diagnostic only fires on a bounded snprintf whose return
is discarded, so a raw sprintf is invisible to it.

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

* Close the self-test's blind spots around msg[]

Six of eight mutants survived the first version. The neighbour canary
compared against zero, so it saw a stray 'X' but not the stray NUL an
off-by-one terminator actually writes; poison it instead. Only the
over-capacity case existed, so padding every message to 79 bytes or eating
its last character both passed, as did a sign-extended 0xff reading as EOF
and the new unparseable-status branch, which had no coverage at all.

Same zero-comparison weakness applied to the htsblk_failf canary inherited
from #715, so that one is poisoned too.

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 18:51:41 +02:00
Xavier Roche
dc6819b7b7 Cached headers longer than ProxyTrack's fields overflow into the location pointer (#717)
* Bound ProxyTrack's cache header copies

ZIP_READFIELD_STRING in proxy/store.c took no destination size and used a
raw strcpy, while the engine's namesake in htscache.c has taken a
refvalue_size for years. The source is a cache-entry header line bounded
only by line[HTS_URLMAXSIZE + 2].

contenttype[64] sits just before the location pointer in the calloc'd
_PT_Element, so an over-long Content-Type walks over charset and then
over location, which the next Location: line copies through.

Clip rather than reject: the engine stores Content-Type in 128 bytes, so
a valid cache legitimately carries fields wider than ours.

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

* Bound the ARC reader's header copies too

Review found store.c carries a second copy of the same macro:
HTTP_READFIELD_STRING feeds the ARC reader from index->line[2048], twice
the ZIP path's reach, into the same contenttype[64] and its neighbouring
location pointer. proxytrack --convert on a plain-text ARC segfaults.

Test 86 covered one of eight fields, so a per-field clip and a
one-size-fits-all clip were indistinguishable. It now overshoots every
destination and asserts each surviving length against its own capacity,
and exercises the ARC reader without needing python.

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 18:51:13 +02:00
Xavier Roche
3247e5b183 Diagnostic messages built from remote replies discard snprintf's truncation signal (#715)
* Fold htsblk/PT_Element failure messages into a clipping helper

The 18 remaining -Wformat-truncation warnings all came from building a
diagnostic string out of a remote server's reply and dropping snprintf's
return. Add htsblk_failf() for htsblk.msg[80] and PT_Element_failf() for
ProxyTrack's msg[1024]: both clip to fit and absorb the discard once, so
the obligation is not silently laundered at each call site.

msg[80] lives in the installed htsopt.h, so growing it would break the
ABI; a clipped FTP banner is the intended outcome anyway. The display
StatsBuffer.state is not installed, so it grows to fit back->info instead.

Also bounds two unbounded sprintf(r->msg, ...) in ProxyTrack's cache
reader and moves the raw strcpy neighbours to strcpybuff.

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

* Catch a one-past-the-end write into msg[]'s neighbour

Review found the new self-test blind to a store at msg[sizeof(msg)]: it
lands in the adjacent contenttype field, which no assertion read, and an
intra-struct overflow is invisible to ASan and _FORTIFY_SOURCE. Check the
neighbour after every call, and add the exact-fit case the block lacked.

Correct the contract comment too: msg is not purely diagnostic, it
round-trips through the cache as X-StatusMessage.

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 18:00:21 +02:00
Xavier Roche
bef7120423 tests: test 82 fails on master after #707 and #709 landed together (#716)
* tests: arm test 82's mirror root through a profile save

#707 made /website/ serve only the root htsserver recorded at structcheck
success, so the posted projpath test 82 relied on no longer names anything.
Each PR was green alone; the pair was not.

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

* tests: drop the now-unused project-path argument

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 17:44:55 +02:00
Xavier Roche
7cf54af485 htsserver: a posted projpath repoints the served root and overflows the composed path (#707)
* htsserver builds the redirect Location header in a 256-byte stack buffer

The POST redirect path checks strlen(file) but sprintf's newfile, which comes
straight from the client's "redirect" POST field with no length cap. A 300-byte
value overflows tmp[256]. The same value reached the Location header with no
CR/LF check, so it could split the response and inject headers.

Append into the dynamic String the other headers already use, and drop the
header entirely when the value carries a CR or LF.

The listen socket was SOCaddr_initany, so the server answered the LAN and not
just the local browser it exists to serve. Bind 127.0.0.1 by default, with
--bind <addr> to widen it again, resolved through the existing gethost() helper
the way proxytrack already does.

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

* tests: satisfy shellcheck and shfmt in the new server test

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

* tests: drop the pre-fix narration from the oversized-value comment

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

* tests: assert the bound socket, not the announced URL

The listen-address assertions only compared the URL= banner, which is a
literal echo of argv: a build that announced 127.0.0.1 while binding the
wildcard passed. Probe 127.0.0.2 on the same port instead, which a wildcard
listener takes and a loopback-only one leaves free.

Also refuse an empty --bind, which fell through to every interface and
silently undid the new default.

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

* tests: bound the response read and the empty --bind run

The recv() loop had no timeout and read until EOF; htsserver need not close
the connection after responding, which wedged the macOS runner for over an
hour. Stop at the end of the header block, which is all the test reads.

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

* htsserver: the session id must gate the request body, not the reply

Every field of a POST body is written straight into the one global key store
the templates and the command dispatcher both read, and "command" from there
reaches the engine. The gate ran after that write and compared "sid" against
"_sid" -- but "_sid" is copied into "sid" beforehand so the templates can
render it, so a request that simply omitted the field compared equal to
itself. Only a wrong id was refused; an absent one passed. Clearing the reply
afterwards does not help either, because the dispatcher sits outside the reply
guard.

Authenticate before parsing instead: scan the raw body for "sid", require at
least one occurrence and reject if any of them differs, and drop the body
untouched when it does not match. That leaves the shared template key alone,
and it closes the dispatcher for free.

A refused request also emitted only a Content-length line, since the status
line for that branch was behind _DEBUG. Any client reads that as a protocol
error, which is how test 68 failed rather than reporting the refusal. Send a
403 instead.

Tests 68 and 77 posted without an id, which is what the engine used to accept,
so both now fetch the one the server renders into the form. Test 78 covers
accept, missing, empty and wrong, asserts the 403, and probes the key store
through ${projname} rather than the suppressed reply -- a reply-only assertion
passes even when the write goes through.

Also fix a leak that hung macOS CI: start() runs inside a command
substitution, so its $! never reached the parent and stop() guarded on an
empty variable, leaving one htsserver per call. Test 77 starts four, which is
exactly the four orphans the runner reported while sitting for half an hour
behind a green test log. Take the pid from the PID= line the server already
announces.

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

* htsserver: serve the crawled mirror verbatim, never through the expander

WebHTTrack serves the mirror under /website/ from the same small server as its
own GUI, and the decision to run a response through the ${...} template
expander was a substring test for ".htm" on the request path. A mirrored page
therefore had its directives evaluated: ${_sid} rendered the live session id,
handing the crawled site the token that authenticates commands on the local
GUI, and ${do:...} gave it the rest of the template verbs.

The /website/ prefix was already detected, but only to keep the crawl-state
override from hijacking a mirror request. Reuse it as the expansion gate, so
expansion is limited to files under the GUI's html root, and re-evaluate it
after that override, which can substitute a GUI page for a mirror path.
Mirrored pages keep their text/html type: verbatim must not turn browsing the
mirror into a download.

Note that mirrored content still shares the control origin, so a script in it
can read the session id from a GUI page itself; that is a separate fix.

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

* htsserver: stop letting a posted projpath name the /website/ root

The static-file path in smallserver() was composed with a length guard that
measured only the server root and the request path, then wrote the posted
"projpath" field into a 1024-byte stack buffer: a 2000-byte projpath followed
by any /website/ request smashed the stack (buffer overflow detected, server
gone). The guard also summed two untrusted lengths before comparing, the shape
that can wrap and pass.

Composition now goes through a bounded, non-aborting append that keeps the
untrusted length alone on one side, and /website/ is served from the project
directory the server itself set up, rejecting a ".." in it, rather than from
whatever root the request body claimed. Without that, projpath=/etc/ plus
GET /website/passwd read an arbitrary file, since the ".." check looked at the
request path only. fsfile is also cleared before the error-redirect branch,
which could otherwise reach fopen() uninitialized.

tests/80_webhttrack-projpath.test drives the three cases against a live server
and keeps a legitimate project browsable as the control.

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

* tests: establish test 79's mirror root through the server

/website/ no longer serves the posted projpath, so injecting one as a
fixture stopped working and test 79 got a 404 instead of the mirrored
page. Save a profile first (no command_do=start, so nothing crawls) to
make the server record the root, then plant the file under it.

Re-checked against a reverted expander gate: still fails there, so the
fixture change did not cost the assertion its teeth.

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

* tests: cover the running-crawl half of the /website/ override

Test 84 only exercised the idle server, where the override never fires and
the recomputed virtualpath is indistinguishable from the stale one. Drive a
crawl through the server so /website/*.html is rewritten to the GUI refresh
page, which 404s without the recompute.

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

* tests: cover the overflow, the '..' rejection and the fsfile hoist

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

* tests: fix the merged test 84 duplicate post() helper

The rename-side and the branch-side each defined post(), with different
argument shapes; the last one won and silently mangled the save body.

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

* tests: trim the new comments

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

* tests: drop the stale start() arg comments in test 84

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Signed-off-by: Xavier Roche <xroche@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 17:18:43 +02:00
Xavier Roche
c32a47110e webhttrack: the mirror link on the finished page cannot be followed (#709)
* WebHTTrack's mirror links are dead file:// URLs

The GUI is served from http://127.0.0.1:PORT/, and browsers refuse to navigate
from an http: page to a file: URL, so the "browse the mirror" links in
finished.html and file.html did nothing when clicked. They now point at the
server's own /website/ route, which already serves the project directory. The
desktop entry's browse mode still works, because there the shell hands the
file: URL to the browser instead of navigating from a page.

The per-project picker in file.html goes too: /website/ is bound to the running
project, and reaching the other projects over HTTP would mean serving the whole
mirror tree from the control origin.

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

* finished.html: link the served mirror, not a dead file:// URL

The page is served over http:, so its file:// link is a cross-scheme
navigation that Chrome and Firefox refuse. Nothing happened on click. The
mirror is already reachable at /website/, which the two list entries just
below were using all along.

file.html keeps its file:// links. It is the fresh-session entry point for
sites mirrored earlier, and /website/ is bound to one project, so pointing
it there would trade a dead link for a 404. Serving an arbitrary past
project needs a route that does not exist yet.

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

* tests: tighten the browse-link assertion and trim its comments

The runtime check for href="/website/index.html" also matched the list
entry below the anchor, so it passed on the unfixed page; match the
anchor by its mirror-path label instead.

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 17:18:17 +02:00
Xavier Roche
1027a9f392 htsserver: crawled pages run through the template expander and leak the session id (#706)
* htsserver builds the redirect Location header in a 256-byte stack buffer

The POST redirect path checks strlen(file) but sprintf's newfile, which comes
straight from the client's "redirect" POST field with no length cap. A 300-byte
value overflows tmp[256]. The same value reached the Location header with no
CR/LF check, so it could split the response and inject headers.

Append into the dynamic String the other headers already use, and drop the
header entirely when the value carries a CR or LF.

The listen socket was SOCaddr_initany, so the server answered the LAN and not
just the local browser it exists to serve. Bind 127.0.0.1 by default, with
--bind <addr> to widen it again, resolved through the existing gethost() helper
the way proxytrack already does.

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

* tests: satisfy shellcheck and shfmt in the new server test

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

* tests: drop the pre-fix narration from the oversized-value comment

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

* tests: assert the bound socket, not the announced URL

The listen-address assertions only compared the URL= banner, which is a
literal echo of argv: a build that announced 127.0.0.1 while binding the
wildcard passed. Probe 127.0.0.2 on the same port instead, which a wildcard
listener takes and a loopback-only one leaves free.

Also refuse an empty --bind, which fell through to every interface and
silently undid the new default.

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

* tests: bound the response read and the empty --bind run

The recv() loop had no timeout and read until EOF; htsserver need not close
the connection after responding, which wedged the macOS runner for over an
hour. Stop at the end of the header block, which is all the test reads.

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

* htsserver: the session id must gate the request body, not the reply

Every field of a POST body is written straight into the one global key store
the templates and the command dispatcher both read, and "command" from there
reaches the engine. The gate ran after that write and compared "sid" against
"_sid" -- but "_sid" is copied into "sid" beforehand so the templates can
render it, so a request that simply omitted the field compared equal to
itself. Only a wrong id was refused; an absent one passed. Clearing the reply
afterwards does not help either, because the dispatcher sits outside the reply
guard.

Authenticate before parsing instead: scan the raw body for "sid", require at
least one occurrence and reject if any of them differs, and drop the body
untouched when it does not match. That leaves the shared template key alone,
and it closes the dispatcher for free.

A refused request also emitted only a Content-length line, since the status
line for that branch was behind _DEBUG. Any client reads that as a protocol
error, which is how test 68 failed rather than reporting the refusal. Send a
403 instead.

Tests 68 and 77 posted without an id, which is what the engine used to accept,
so both now fetch the one the server renders into the form. Test 78 covers
accept, missing, empty and wrong, asserts the 403, and probes the key store
through ${projname} rather than the suppressed reply -- a reply-only assertion
passes even when the write goes through.

Also fix a leak that hung macOS CI: start() runs inside a command
substitution, so its $! never reached the parent and stop() guarded on an
empty variable, leaving one htsserver per call. Test 77 starts four, which is
exactly the four orphans the runner reported while sitting for half an hour
behind a green test log. Take the pid from the PID= line the server already
announces.

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

* htsserver: serve the crawled mirror verbatim, never through the expander

WebHTTrack serves the mirror under /website/ from the same small server as its
own GUI, and the decision to run a response through the ${...} template
expander was a substring test for ".htm" on the request path. A mirrored page
therefore had its directives evaluated: ${_sid} rendered the live session id,
handing the crawled site the token that authenticates commands on the local
GUI, and ${do:...} gave it the rest of the template verbs.

The /website/ prefix was already detected, but only to keep the crawl-state
override from hijacking a mirror request. Reuse it as the expansion gate, so
expansion is limited to files under the GUI's html root, and re-evaluate it
after that override, which can substitute a GUI page for a mirror path.
Mirrored pages keep their text/html type: verbatim must not turn browsing the
mirror into a download.

Note that mirrored content still shares the control origin, so a script in it
can read the session id from a GUI page itself; that is a separate fix.

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

* tests: cover the running-crawl half of the /website/ override

Test 84 only exercised the idle server, where the override never fires and
the recomputed virtualpath is indistinguishable from the stale one. Drive a
crawl through the server so /website/*.html is rewritten to the GUI refresh
page, which 404s without the recompute.

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Signed-off-by: Xavier Roche <xroche@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 16:57:59 +02:00
Xavier Roche
d0a1573a04 webhttrack: "max site size" sets a per-file cap and discards the HTML one (#708)
* webhttrack: "max site size" set a per-file cap instead of the overall one

step4.html mapped all three size fields of the wizard onto --max-files (-m),
so "Max site size" emitted a per-file limit rather than --max-size (-M), and
landed a second -m on the command line. That second -m also clobbered the HTML
per-file limit: a bare -m<n> resets maxfile_html, so whichever of the two came
last won. Point sizemax at --max-size and emit the bare -m before the -m,<n>
form so both per-file caps survive.

Two template typos in the same family, where a malformed ${...} renders as
nothing or as its own key instead of erroring: the winprofile.ini writer's
Dos=${dos was missing its closing brace, and option2b.html's OK button read
${LANG_OK] with a bracket.

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

* tests: carry the session id in test 81's POST

The gate that landed with #700 refuses a body without one, so the wizard
POST came back refused and the option audit had nothing to read.

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

* tests: close the confirmation-biased gaps in test 81

Post the size fields empty too, so a step4.html that lost its ${test:} guard
and rendered a valueless --max-files= is caught; assert the winprofile.ini
MaxHtml/MaxOther/MaxAll keys the header claimed to audit; and pin the OK button
label, since an unknown ${LANG_} key renders empty and passed the absence check.

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 16:40:40 +02:00
Xavier Roche
4362ac34cb Check snprintf truncation where the result must be complete (#711)
Adds `sprintfbuff()`/`slprintfbuff()` to `htssafe.h`: a formatted print that truncates to fit and returns whether it had to, marked `warn_unused_result` so the answer cannot be dropped. It fills the gap between the `strcpybuff` family, which aborts on overflow, and `String`, which grows without bound. Abort is the wrong contract wherever the text is built from a remote peer's reply.

Four `-Wformat-truncation=` sites used the result as if `snprintf` had never truncated. Two only needed a bigger destination, so they get one: `hts_finish_makeindex`'s `tempo` was a flat 1024 against a 2048-byte escaped URL and is now sized off it, and the wizard's `cmd[4096]` could not hold the answers it concatenates. The other two cannot grow. `create_back_tmpfile` formats `<url_sav>.bak` into a buffer the same size as `url_sav`, and that struct is installed, so a dropped extension would alias the backup onto the live file that `back_finalize_backup()` unlinks. ProxyTrack's `startUrl[1024]` is fed by cache content. Both take the error path they already had, and ProxyTrack moves to the next cache entry rather than publishing a clipped one.

On what the wrapper buys, since it is not what I first assumed: checking a raw `snprintf` return inline silences the warning just as well. The wrapper's value is that the capacity comes from `sizeof`, the check is the default rather than the exception, and `warn_unused_result` makes skipping it visible.

`-#test=strsafe` covers the primitive (exact fit, one over, 4 KB source, `size == 1`, trailing canary, destination repoisoned between cases), and `-#test=makeindex` gains a first link whose escaped form overruns the old buffer. Both were mutation-checked. The ProxyTrack change ships without a direct test: its only observable is the catalog page, which renders solely as a PROPFIND fallback I could not drive from curl. 25 gcc warnings down to 21; the rest of the cluster is diagnostic-only and follows separately.

Signed-off-by: Xavier Roche <xroche@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 16:37:48 +02:00
Xavier Roche
6579436607 webhttrack: the command line overflows its argv vector and a quoted value can inject flags (#710)
* htsserver builds the redirect Location header in a 256-byte stack buffer

The POST redirect path checks strlen(file) but sprintf's newfile, which comes
straight from the client's "redirect" POST field with no length cap. A 300-byte
value overflows tmp[256]. The same value reached the Location header with no
CR/LF check, so it could split the response and inject headers.

Append into the dynamic String the other headers already use, and drop the
header entirely when the value carries a CR or LF.

The listen socket was SOCaddr_initany, so the server answered the LAN and not
just the local browser it exists to serve. Bind 127.0.0.1 by default, with
--bind <addr> to widen it again, resolved through the existing gethost() helper
the way proxytrack already does.

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

* tests: satisfy shellcheck and shfmt in the new server test

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

* tests: drop the pre-fix narration from the oversized-value comment

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

* tests: assert the bound socket, not the announced URL

The listen-address assertions only compared the URL= banner, which is a
literal echo of argv: a build that announced 127.0.0.1 while binding the
wildcard passed. Probe 127.0.0.2 on the same port instead, which a wildcard
listener takes and a loopback-only one leaves free.

Also refuse an empty --bind, which fell through to every interface and
silently undid the new default.

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

* tests: bound the response read and the empty --bind run

The recv() loop had no timeout and read until EOF; htsserver need not close
the connection after responding, which wedged the macOS runner for over an
hour. Stop at the end of the header block, which is all the test reads.

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

* htsserver: the session id must gate the request body, not the reply

Every field of a POST body is written straight into the one global key store
the templates and the command dispatcher both read, and "command" from there
reaches the engine. The gate ran after that write and compared "sid" against
"_sid" -- but "_sid" is copied into "sid" beforehand so the templates can
render it, so a request that simply omitted the field compared equal to
itself. Only a wrong id was refused; an absent one passed. Clearing the reply
afterwards does not help either, because the dispatcher sits outside the reply
guard.

Authenticate before parsing instead: scan the raw body for "sid", require at
least one occurrence and reject if any of them differs, and drop the body
untouched when it does not match. That leaves the shared template key alone,
and it closes the dispatcher for free.

A refused request also emitted only a Content-length line, since the status
line for that branch was behind _DEBUG. Any client reads that as a protocol
error, which is how test 68 failed rather than reporting the refusal. Send a
403 instead.

Tests 68 and 77 posted without an id, which is what the engine used to accept,
so both now fetch the one the server renders into the form. Test 78 covers
accept, missing, empty and wrong, asserts the 403, and probes the key store
through ${projname} rather than the suppressed reply -- a reply-only assertion
passes even when the write goes through.

Also fix a leak that hung macOS CI: start() runs inside a command
substitution, so its $! never reached the parent and stop() guarded on an
empty variable, leaving one htsserver per call. Test 77 starts four, which is
exactly the four orphans the runner reported while sitting for half an hour
behind a green test log. Take the pid from the PID= line the server already
announces.

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

* webhttrack: bound the argv vector and escape quotes in the wizard command line

The wizard hands its httrack command line to the engine as one string, which
back_launch_cmd() split back into argv. Two things were wrong with that split.

It wrote into a fixed 1024-pointer vector with no bound, and every unquoted
space in the posted string yields an entry, so an ordinary mirror with a few
hundred URLs walked off the allocation. The split now lives in htscmdline.c as
hts_split_cmdline(), which sizes the vector from the separator count before
filling it, and the engine self-tests can reach it.

Quotes were also purely advisory: they toggled the "inside an argument" state
but nothing escaped them, so a double quote typed into a wizard field (user
agent, footer, path, project name) closed the argument early and the rest of
the value was parsed as fresh options -- among them -V, which reaches system().
Escaping has to happen where the argument boundary is known, so the template
gets its own ${arg:} filter for that context; ${html:} keeps its meaning for
the HTML attributes it is used in everywhere else, and HTML escaping would not
help anyway since the browser undoes it when it posts the command line back.
${arg:} backslash-escapes a quote and a backslash, and the splitter reads those
inside a quoted run, the same convention next_token() already implements for
doit.log. A value containing a quote now survives it intact instead of turning
into options.

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

* webhttrack: keep a stray quote in an unquoted field out of the split

The url and wildcard-filter fields go into the command line outside quotes,
where no backslash can escape anything: a single quote there flips the parity
of every quote after it, so a later escaped value ends up split as flags and
the escaping buys nothing. Emit %22 for those fields instead.

NULL-terminate the argv vector while here, matching the convention the tree
documents in htscharset.c, and fold the third copy of the entity table into
one helper.

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

* tests: do not feed snprintf's return value back as its size argument

snprintf returns the length it wanted to write, so accumulating it blind
lets the next size argument wrap. The buffer is sized well past what the
loop needs, but the pattern is the one the project forbids.

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Signed-off-by: Xavier Roche <xroche@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 16:26:55 +02:00
Xavier Roche
cb98b1b197 Fatal-signal backtraces are unreadable: every engine frame is a bare module+offset (#705)
* Symbolize fatal-signal backtraces through addr2line

backtrace_symbols_fd() resolves names from .dynsym only, and
-fvisibility=hidden keeps every engine frame out of it, so a crash report
arrived as a column of bare module+offset. The handler now emits the raw trace
first and unconditionally, then groups the frames per module and runs addr2line
(or llvm-symbolizer) over the offsets, which reads DWARF and names the static
frames plus their inline chain.

-rdynamic is dropped: it only populated .dynsym and bought exactly one named
frame. -Wl,--build-id replaces it, so a trace from a stripped build can be
matched to its debug symbols.

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

* Move the crash backtrace printer into src/htsbacktrace.c

httrack.c keeps only the two call sites. The symbolizer needs _GNU_SOURCE for
dladdr(), which is now confined to its own translation unit instead of being
forced on the whole CLI front-end.

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

* Test: match glibc's backtrace format with or without the space

backtrace_symbols_fd() prints the trailing "[0xADDR]" with a leading space on
some glibc versions and without on others (Ubuntu 24.04), so the raw-frame
assertion failed everywhere but the dev box. Match only up to the offset.

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

* Skip pseudo-modules with no file on disk

A frame in linux-vdso.so.1 made addr2line complain instead of resolving, so
the arm64 leg lost its symbolized output entirely. Renumber the test too:
77 landed on master with #700.

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

* Stop discarding local symbols, which made the names wrong

--discard-all drops the local symbol of every static function, so addr2line
attributes the frame to the nearest surviving global: the abort frame read
dns_timeout_selftests instead of abortf_, with the file and line still right.
A wrong name is worse than none, and because the symbols go at link time no
-dbgsym package can recover them. Costs 21784 bytes on libhttrack.so.3, 0.6%.

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

* Symbolize once when the handler itself faults

A fault inside the handler re-enters the printer, which interleaved a second
symbolized trace on the same fd and spent a second budget: 3.04s and two
overlapping reports, measured.

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

---------

Signed-off-by: Xavier Roche <xroche@gmail.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 15:43:17 +02:00
Xavier Roche
da8fbfff49 Fix dead array-address checks and discarded-qualifier casts (-Waddress, -Wcast-qual) (#703)
* Drop NULL tests on inline array members

`lien_back::url_sav`, `htsblk::msg` and POSIX `dirent::d_name` are arrays,
so testing their address folds to a constant and gcc/clang report it
(-Waddress, -Wpointer-bool-conversion). Every site keeps whatever real
condition sat beside the dead one, so behavior is unchanged.

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

* help_wizard: check the allocation, not the arrays it contains

The out-of-memory guard has been constant-false since d593418 folded the
nine separate wizard buffers into one struct: the names it tests are now
inline arrays, so `malloct()`'s result is never checked and an exhausted
heap gets a NULL-page write instead of the intended message. Also switch
the raw free() to freet() and release the struct on the two early returns
that leaked it.

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

* proxytrack: test the WebDAV header fields, not their addresses

`PT_Element::lastmodified` and `::contenttype` are inline arrays, so both
`if`s were constant-true (-Waddress); use the `[0]` form the same file
already uses when it emits the GET headers. Neither is observable:
get_time_rfc822("") returns 0 and falls through to the index timestamp,
and proxytrack_add_DAV_Item already substitutes application/octet-stream
for an empty mime, which a PROPFIND probe against a cache entry carrying
no Content-Type confirms both before and after.

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

* Fix the discarded const qualifiers rather than casting them away

`binput` and `cache_binput` only ever read through their source pointer,
so they take `const char *` now; that alone clears the cast in
htsrobots.c, and neither is exported nor declared in an installed header,
so no ABI question arises. `treathead` keeps `char *rcvd` because it does
NUL-cut the header in place, and the two selftest calls that fed it a
string literal get a mutable buffer instead, matching their three
siblings and removing a latent write to .rodata. The remaining two are
one-liners: zlib's `next_in` is already `const` under -DZLIB_CONST, and
htsback can call the non-const `jump_protocol` twin on its mutable
`url_adr`. libhttrack.vcxproj gains ZLIB_CONST so the MSVC build agrees
with autotools, as webhttrack and proxytrack already do.

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

* tests: prove the WebDAV mime/timestamp fallback survives an empty field

proxytrack's DAV PROPFIND response computes a fallback content-type and
timestamp when a cache entry has no Content-Type/Last-Modified; that
fallback already existed before commit eae1dd0 changed the surrounding
always-true array-address checks, so this test guards the equivalence
rather than a bug. Verified it fails when the fallback default is
disabled, and passes unmodified against the pre-eae1dd0 code too.

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

* tests: bound the PROPFIND request and cut the header comment

curl had no --max-time; an unbounded read wedges the runner instead of
failing it.

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

* tests: skip the WebDAV mime test on Windows

It is the first test to run proxytrack as a live listener, and MSYS cannot
reap a native one: the orphan wedged the whole Windows suite past its
45-minute budget, twice, destroying the log upload with it.

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 14:49:29 +02:00
247 changed files with 13684 additions and 1159 deletions

View File

@@ -46,10 +46,32 @@ jobs:
- name: Configure
run: |
set -euo pipefail
# Regenerate from configure.ac/Makefile.am to validate them; the
# committed generated files already let a plain checkout build.
# Regenerate: configure and the Makefile.in's are not tracked.
autoreconf -fi
# Disabling zlib must fail here rather than at link with a pile of
# undefined minizip references (#735). Both spellings, so a rewrite
# cannot keep one and lose the other. Probed out-of-tree to leave
# nothing behind.
nozlib="$RUNNER_TEMP/nozlib"
for arg in --without-zlib --with-zlib=no; do
rm -rf "$nozlib" && mkdir -p "$nozlib"
if (cd "$nozlib" && "$GITHUB_WORKSPACE/configure" "$arg" >out.log 2>&1); then
echo "::error::configure $arg succeeded; it must be rejected"
exit 1
fi
# ... and for the stated reason, not an unrelated configure failure.
grep -q "zlib cannot be disabled" "$nozlib/out.log" \
|| { cat "$nozlib/out.log"; exit 1; }
done
./configure
# Same dead end from the compile side. The bare compile is the
# control: without it a broken probe would pass vacuously.
hdr='#include "htsglobal.h"'
echo "$hdr" | $CC -I. -Isrc -fsyntax-only -xc -
if echo "$hdr" | $CC -DHTS_USEZLIB=0 -I. -Isrc -fsyntax-only -xc - 2>/dev/null; then
echo "::error::-DHTS_USEZLIB=0 compiled; htsglobal.h must reject it"
exit 1
fi
# a missing decoder would silently drop the coding from Accept-Encoding
grep -q "define HTS_USEBROTLI 1" config.h
grep -q "define HTS_USEZSTD 1" config.h
@@ -57,7 +79,10 @@ jobs:
- name: Build
run: make -j"$(nproc)"
# 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
timeout-minutes: 20
run: |
jobs=$(( $(nproc) * 2 )); [ "$jobs" -le 16 ] || jobs=16
make check -j"$jobs"
@@ -97,6 +122,7 @@ jobs:
run: make -j"$(nproc)"
- name: Test without python3
timeout-minutes: 20
run: |
set -euo pipefail
# Hide every python3* so `command -v python3` fails like it does in the
@@ -151,6 +177,7 @@ jobs:
sudo ifconfig lo0 alias 127.0.0.3 up
- name: Test
timeout-minutes: 20
run: |
jobs=$(( $(sysctl -n hw.ncpu) * 2 )); [ "$jobs" -le 16 ] || jobs=16
make check -j"$jobs"
@@ -223,6 +250,7 @@ jobs:
run: make -j"$(nproc)"
- name: Test
timeout-minutes: 20
run: |
jobs=$(( $(nproc) * 2 )); [ "$jobs" -le 16 ] || jobs=16
make check -j"$jobs"
@@ -280,6 +308,7 @@ jobs:
env:
ASAN_OPTIONS: detect_leaks=0:abort_on_error=1:halt_on_error=1:strict_string_checks=1:malloc_fill_byte=202:max_malloc_fill_size=2147483647:free_fill_byte=203:max_free_fill_size=2147483647
UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1
timeout-minutes: 20
run: |
jobs=$(( $(nproc) * 2 )); [ "$jobs" -le 16 ] || jobs=16
make check -j"$jobs"
@@ -323,6 +352,7 @@ jobs:
- name: Test (offline self-tests under MSan)
env:
MSAN_OPTIONS: abort_on_error=1:halt_on_error=1
timeout-minutes: 20
run: |
set -euo pipefail
# 01_engine-* only; zlib-dependent self-tests are named 01_zlib-* and
@@ -404,6 +434,7 @@ jobs:
run: make -j"$(nproc)"
- name: Test
timeout-minutes: 20
run: |
jobs=$(( $(nproc) * 2 )); [ "$jobs" -le 16 ] || jobs=16
make check -j"$jobs"
@@ -596,7 +627,12 @@ jobs:
set -euo pipefail
git fetch --no-tags origin \
"+refs/heads/${{ github.base_ref }}:refs/remotes/origin/${{ github.base_ref }}"
base="origin/${{ github.base_ref }}"
# Merge base, not the branch tip: master moves during a run, and a
# tip-relative diff blames its new C on this PR.
if ! base="$(git merge-base "origin/${{ github.base_ref }}" HEAD)"; then
echo "::error::no merge base with origin/${{ github.base_ref }}; cannot scope the check."
exit 1
fi
set +e
diff="$(git clang-format --binary clang-format-19 --style=file \
--diff --extensions c,h "$base")"
@@ -638,6 +674,8 @@ jobs:
git fetch --no-tags origin \
"+refs/heads/${{ github.base_ref }}:refs/remotes/origin/${{ github.base_ref }}"
base="origin/${{ github.base_ref }}"
# Three dots: merge-base scoped, so commits landing on master mid-run
# are not counted as this PR's.
changed="$(git diff --name-only "$base"...HEAD)"
has() { printf '%s\n' "$changed" | grep -qx "$1"; }
if has man/httrack.1 && ! has html/httrack.man.html; then

View File

@@ -189,25 +189,53 @@ jobs:
# A wedged crawl must not eat the job's timeout budget. timeout(1)'s
# signals can't reap a native httrack.exe (MSYS signals don't reach it),
# so a hang orphaned processes that starved the runner; run_with_timeout
# TerminateProcess-es the whole tree. 600s clears the slowest multi-pass
# crawl (a few passes at --max-time=120 each).
# TerminateProcess-es the whole tree. 600s is unchanged: it clears the
# 540s a three-pass crawl may legitimately take under local-crawl.sh's
# own watchdogs, against a slowest healthy test here of 39s.
. ./testlib.sh
per_test=600
pass=0 fail=0 skip=0 failed="" skipped=""
# The whole suite must give up before the step timeout above. A cancelled
# step keeps neither its log nor the artifacts the later if:always()
# steps would upload, so an overrun that ends in a cancel tells us
# nothing; failing on our own terms keeps both. Healthy runs take 8-9
# min. The check sits between tests, so the step can still reach 25 min
# plus one per-test budget, and that worst case stays inside the 45.
suite_deadline=1500
started=$SECONDS
# Survives into the artifact even if the tail of the step log does not.
progress=suite-progress.log
: >"$progress"
pass=0 fail=0 skip=0 failed="" skipped="" deadline=0
for t in 00_runnable.test 01_engine-*.test 01_zlib-*.test \
*_local-*.test 13_crawl_proxy_https.test 58_watchdog.test \
60_crawl-log-salvage.test; do
elapsed=$((SECONDS - started))
if [ "$elapsed" -ge "$suite_deadline" ]; then
echo "::error::suite deadline: ${elapsed}s elapsed, stopping before $t"
echo "DEADLINE before $t after ${elapsed}s" >>"$progress"
# Per-test start times, so the slow ones are named rather than guessed.
sed 's/^/ /' "$progress"
deadline=1
break
fi
echo "RUN $t at ${elapsed}s" >>"$progress"
rc=0
run_with_timeout 600 bash "$t" >"$t.log" 2>&1 || rc=$?
# Same guard "make check" uses on POSIX, so a wedge is diagnosed the
# same way on every platform. It dumps before it kills, which a bare
# run_with_timeout cannot: by the time that returns, the tree whose
# stack we wanted is already gone.
HTTRACK_TEST_TIMEOUT=$per_test bash ./test-timeout.sh "$t" >"$t.log" 2>&1 || rc=$?
case "$rc" in
0) pass=$((pass + 1)); echo "PASS $t" ;;
77) skip=$((skip + 1)) skipped="$skipped $t"; echo "SKIP $t" ;;
124)
fail=$((fail + 1)) failed="$failed $t"
# test-timeout.sh has already written the process list, the stacks
# and the killed crawl's own logs into $t.log.
echo "FAIL $t (timed out, tree killed)"
# Re-running a wedge traced would just hang again: salvage the
# killed crawl's own logs into the artifact instead.
dump_crawl_logs >>"$t.log"
tail -n 25 "$t.log" | sed 's/^/ /'
;;
*)
@@ -215,19 +243,30 @@ jobs:
echo "FAIL $t (exit $rc)"
# These assert with `test "$(...)" == "..." || exit 1`, which
# says nothing at all on failure. Re-run traced, still bounded.
run_with_timeout 600 bash -x "$t" >>"$t.log" 2>&1 || true
run_with_timeout "$per_test" bash -x "$t" >>"$t.log" 2>&1 || true
tail -n 25 "$t.log" | sed 's/^/ /'
;;
esac
echo "$rc $t" >>"$progress"
# An orphaned native httrack.exe spins and starves the runner, which
# is how this job dies with "lost communication" rather than a plain
# timeout. Clear them between tests and name whoever leaked them.
reap_leftover_processes "$t" | tee -a "$progress"
done
echo "ran=$((pass + fail + skip)) pass=$pass fail=$fail skip=$skip" |
tee -a "$GITHUB_STEP_SUMMARY"
# Every gate here exits 77, so an all-skipped suite would report green having
# tested nothing: pin the skips, and floor the passes in case the glob empties.
# footer-overflow skips on Windows (needs a path past MAX_PATH); crange pending #581;
# webdav-mime needs a reapable background listener, which MSYS cannot give it.
expected_skips=" 01_engine-footer-overflow.test 48_local-crange-memresume.test 71_local-crange-repaircache.test 79_local-proxytrack-webdav-mime.test"
# footer-overflow and purge-longpath skip on Windows (need a path past MAX_PATH);
# crange pending #581;
# webdav-default and webdav-mime need a reapable background listener, which MSYS cannot give them;
# badmtime needs a filesystem that stores an mtime past gmtime's range;
# single-file ends on a GUI half needing htsserver, which this job does not build;
# update-304-leak needs a LeakSanitizer build, which MSVC has no equivalent of.
expected_skips=" 01_engine-footer-overflow.test 100_local-purge-longpath.test 114_local-update-304-leak.test 120_local-proxytrack-webdav-default.test 48_local-crange-memresume.test 71_local-crange-repaircache.test 79_local-proxytrack-webdav-mime.test 88_local-proxytrack-badmtime.test 94_local-single-file.test"
# First, or the deadline reads as an unexplained shortfall in the gates below.
[ "$deadline" -eq 0 ] || { echo "::error::suite did not finish within ${suite_deadline}s"; exit 1; }
[ "$pass" -ge 90 ] || { echo "::error::only $pass tests passed ($skip skipped)"; exit 1; }
[ "$skipped" = "$expected_skips" ] || { echo "::error::unexpected skips:$skipped"; exit 1; }
[ "$fail" -eq 0 ] || { echo "::error::failing:$failed"; exit 1; }

View File

@@ -26,6 +26,18 @@ the operational checklist: toolchain, invariants, and how to ship a change.
check`, or `PATH="<bld>/src:$PATH"` for a manual run.
- Give new `.test` scripts `set -e`: the older ones predate the rule, so several
`local-crawl.sh` calls with no `set -e` report PASS on any non-last failure.
- Run teardown with errexit off: `trap 'set +e; cleanup' EXIT`. Under `set -e` a
failing cleanup command becomes the test's exit status (#773). Keep the other
signals on their own `trap` line, or errexit stays off for the rest of the run.
The guard also resets `$?`, so save it first if teardown reads it.
- Never pipe into `grep -q`: it exits on the first match, so whatever the
producer had left to write takes SIGPIPE, and under `pipefail` that becomes
the pipeline's status. `cmd | grep -q M && fail` then never fires and a probe
that proved nothing reads as "marker absent"; `cmd | grep -q M || fail` fails
a test whose marker was present. bash issues one `write()` per line, so any
match that is not on the last line is exposed. Capture the reply, assert the
status line it must carry (an empty, truncated or redirected one is
marker-free too), then match with a here-string: `grep -q M <<<"$reply"`.
## Hard invariants
- **Generated autotools files are NOT in git.** `configure`, every
@@ -48,6 +60,16 @@ the operational checklist: toolchain, invariants, and how to ship a change.
- Bounds-check every copy. Overflow-safe form: put the untrusted value alone,
`untrusted < limit - controlled` — never `controlled + untrusted < limit`,
which can wrap and pass.
- **Abort or clip is a decision, not a default.** The `*_safe_` helpers
(`strcpybuff`, `strlcpybuff`, `strcatbuff`) **abort** on overflow. Right for
our own data, wrong for anything read back from a cache, a header or the
wire, where it trades a memory smash for a crash on malformed input. Clip
with `dst[0] = '\0'; strlncatbuff(dst, src, size, size - 1)`.
- **A warning class is not the unsafe set.** `-Wformat-truncation` fires only on
a *bounded* `snprintf` whose return is discarded, so an unbounded `sprintf`
into the same buffer never appears on it. Before scoping a hardening pass off
compiler output, grep the unguarded forms yourself (`\bsprintf\s*\(`,
`\bstrcpy\s*\(`, `\bstrcat\s*\(`).
## C conventions
- **Use the `*t` allocator wrappers, never raw libc** (`htssafe.h`):
@@ -84,6 +106,17 @@ Before pushing, and when reviewing others, don't skim for bugs:
layout/ABI, cache/wire format, or a security path? A static or unit check
isn't enough; exercise the wrong behavior at runtime. Claude Code:
`/review-recipe`.
- **Poison a canary, never compare it against zero.** Checking that a
neighbouring field is still `'\0'` cannot see the stray NUL an off-by-one
terminator writes — the exact bug the canary is there for. Fill it with a
non-zero byte, and prove it by killing both the stray-`'X'` and the
stray-NUL mutant. Neither ASan nor `_FORTIFY_SOURCE` sees an overflow that
lands inside the same struct.
- **Overshoot every destination, not one.** A bounds test that oversizes a
single field cannot tell a per-field bound from a one-size-fits-all one, nor
from a fix that bounds that field and leaves its neighbours raw. Exercise
each destination the path touches, spanning at least two capacities, and
check what the code actually emits before writing the expected values.
## Commits
- **Sign-off is mandatory.** Every commit carries a `Signed-off-by` trailer:

View File

@@ -100,7 +100,8 @@ AX_CHECK_COMPILE_FLAG([-fstack-protector-strong], [DEFAULT_CFLAGS="$DEFAULT_CFLA
[AX_CHECK_COMPILE_FLAG([-fstack-protector], [DEFAULT_CFLAGS="$DEFAULT_CFLAGS -fstack-protector"], [], [-Werror])], [-Werror])
AX_CHECK_COMPILE_FLAG([-fstack-clash-protection], [DEFAULT_CFLAGS="$DEFAULT_CFLAGS -fstack-clash-protection"], [], [-Werror])
AX_CHECK_COMPILE_FLAG([-fcf-protection], [DEFAULT_CFLAGS="$DEFAULT_CFLAGS -fcf-protection"], [], [-Werror])
AX_CHECK_LINK_FLAG([-Wl,--discard-all], [DEFAULT_LDFLAGS="$DEFAULT_LDFLAGS -Wl,--discard-all"])
# No --discard-all: it drops the local symbols naming every static function, so
# a trace misattributes them to the nearest surviving global. Costs 0.6% size.
AX_CHECK_LINK_FLAG([-Wl,--no-undefined], [DEFAULT_LDFLAGS="$DEFAULT_LDFLAGS -Wl,--no-undefined"])
AX_CHECK_LINK_FLAG([-Wl,-z,relro,-z,now], [DEFAULT_LDFLAGS="$DEFAULT_LDFLAGS -Wl,-z,relro,-z,now"])
AX_CHECK_LINK_FLAG([-Wl,-z,noexecstack], [DEFAULT_LDFLAGS="$DEFAULT_LDFLAGS -Wl,-z,noexecstack"])
@@ -127,8 +128,8 @@ AX_CHECK_LINK_FLAG([-pie], [LDFLAGS_PIE="-pie"])
AC_SUBST([CFLAGS_PIE])
AC_SUBST([LDFLAGS_PIE])
## -rdynamic must be a link flag; DEFAULT_CFLAGS (AM_CPPFLAGS) never reaches the linker.
AX_CHECK_LINK_FLAG([-rdynamic], [DEFAULT_LDFLAGS="$DEFAULT_LDFLAGS -rdynamic"])
# Ties a crash trace from a stripped build back to its separate debug symbols.
AX_CHECK_LINK_FLAG([-Wl,--build-id], [DEFAULT_LDFLAGS="$DEFAULT_LDFLAGS -Wl,--build-id"])
### Check for -fvisibility=hidden support
gl_VISIBILITY
@@ -174,7 +175,7 @@ AX_CHECK_ALIGNED_ACCESS_REQUIRED
# check for various headers
AC_CHECK_HEADERS([execinfo.h sys/ioctl.h])
### zlib
### zlib (mandatory)
CHECK_ZLIB()
### brotli and zstd content codings (optional)

View File

@@ -2,7 +2,7 @@
if FUZZERS
noinst_PROGRAMS = fuzz-charset fuzz-meta fuzz-idna fuzz-entities \
fuzz-unescape fuzz-filters fuzz-url fuzz-header fuzz-cachendx \
fuzz-htsparse
fuzz-htsparse fuzz-singlefile fuzz-sitemap
endif
AM_CPPFLAGS = \
@@ -27,6 +27,8 @@ fuzz_url_SOURCES = fuzz-url.c fuzz.h
fuzz_header_SOURCES = fuzz-header.c fuzz.h
fuzz_cachendx_SOURCES = fuzz-cachendx.c fuzz.h
fuzz_htsparse_SOURCES = fuzz-htsparse.c fuzz.h
fuzz_singlefile_SOURCES = fuzz-singlefile.c fuzz.h
fuzz_sitemap_SOURCES = fuzz-sitemap.c fuzz.h
# List corpus files explicitly: automake does not expand EXTRA_DIST globs.
EXTRA_DIST = README.md run-fuzzers.sh \
@@ -47,4 +49,10 @@ EXTRA_DIST = README.md run-fuzzers.sh \
corpus/cachendx/regress-overadvance.bin \
corpus/cachendx/regress-truncated-entry.bin \
corpus/htsparse/basic.html corpus/htsparse/script-inscript.html \
corpus/htsparse/meta-usemap.html corpus/htsparse/malformed.html
corpus/htsparse/meta-usemap.html corpus/htsparse/malformed.html \
corpus/singlefile/img-src.html corpus/singlefile/link-rel.html \
corpus/singlefile/style-block.html corpus/singlefile/style-attr.html \
corpus/singlefile/srcset.html corpus/singlefile/rawtext.html \
corpus/singlefile/malformed.html corpus/singlefile/many-attrs.html \
corpus/sitemap/urlset.xml corpus/sitemap/sitemapindex.xml \
corpus/sitemap/truncated.xml corpus/sitemap/urlset.xml.gz

View File

@@ -0,0 +1,3 @@
<img src="a.png">
<img src=big.png alt=over-cap>
<img src="../escape.png"><img src="/abs.png"><img src="data:,x">

View File

@@ -0,0 +1,4 @@
<link rel="stylesheet" href="s.css">
<link rel=icon href=a.png>
<link rel="next" href="p2.html">
<link rel="preload" href="j.js">

View File

@@ -0,0 +1,5 @@
<img src="unterminated.png
<div style="background:url(a.png">
<style>@import url(
<!-- unterminated comment
<a href=

View File

@@ -0,0 +1 @@
<img src="a.png" a0="v" a1="v" a2="v" a3="v" a4="v" a5="v" a6="v" a7="v" a8="v" a9="v" a10="v" a11="v" a12="v" a13="v" a14="v" a15="v" a16="v" a17="v" a18="v" a19="v" a20="v" a21="v" a22="v" a23="v" a24="v" a25="v" a26="v" a27="v" a28="v" a29="v" a30="v" a31="v" a32="v" a33="v" a34="v" a35="v" a36="v" a37="v" a38="v" a39="v" a40="v" a41="v" a42="v" a43="v" a44="v" a45="v" a46="v" a47="v" a48="v" a49="v" a50="v" a51="v" a52="v" a53="v" a54="v" a55="v" a56="v" a57="v" a58="v" a59="v" a60="v" a61="v" a62="v" a63="v" a64="v" a65="v" a66="v" a67="v" a68="v" a69="v">

View File

@@ -0,0 +1,4 @@
<script>var s="</scripting>"; if(a</b) x=1;</script>
<script src="j.js"></script>
<textarea></textareas></textarea>
<title></titles></title>

View File

@@ -0,0 +1,2 @@
<img srcset="a.png 1x, big.png 2x, a.png 100w">
<source srcset="a.png,, a.png 2x," src="a.png">

View File

@@ -0,0 +1,2 @@
<div style="background:url(a.png);list-style:url('a.png')"></div>
<p style='background:url("a.png")'>x</p>

View File

@@ -0,0 +1,5 @@
<style>@import "s.css";
@import url(sub/b.css);
div{background:url(a.png)}
/* url(a.png) */ p:after{content:"url(a.png)"}
</style>

View File

@@ -0,0 +1 @@
<sitemapindex><sitemap><loc>http://h.test/s2.xml.gz</loc></sitemap></sitemapindex>

View File

@@ -0,0 +1 @@
<urlset><loc>http://h.test/x

View File

@@ -0,0 +1 @@
<?xml version="1.0"?><urlset><url><loc>http://h.test/a.html</loc></url><url><loc>https://h.test/b?x=1&amp;y=2</loc></url></urlset>

Binary file not shown.

131
fuzz/fuzz-singlefile.c Normal file
View File

@@ -0,0 +1,131 @@
/* ------------------------------------------------------------ */
/*
HTTrack Website Copier, Offline Browser for Windows and Unix
Copyright (C) 2026 Xavier Roche and other contributors
SPDX-License-Identifier: GPL-3.0-or-later
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Ethical use: we kindly ask that you NOT use this software to harvest email
addresses or to collect any other private information about people. Doing so
would dishonor our work and waste the many hours we have spent on it.
Please visit our Website: http://www.httrack.com
*/
/* Fuzz the --single-file rewriter (htssinglefile.c): hostile HTML walked
through the tag, CSS url()/@import and srcset parsers, then re-serialized.
The resolver is aimed at a private temp tree, so the inlining half (MIME
guess, base64, nested stylesheet) is reached and nothing else on disk is. */
#include "fuzz.h"
#include "httrack-library.h"
#include "htssinglefile.h"
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
/* Between a.png and big.png, so one input reaches both the inline path and the
over-cap fallback. */
#define FUZZ_SF_CAP 64
static char sf_root[512];
static char sf_page[600];
/* The asset tree, in removal order: the subdirectory comes after its file. */
static const char *const sf_files[] = {"a.png", "big.png", "j.js", "s.css",
"sub/b.css", "sub", NULL};
static void sf_cleanup(void) {
char path[700];
int i;
for (i = 0; sf_files[i] != NULL; i++) {
snprintf(path, sizeof(path), "%s/%s", sf_root, sf_files[i]);
(void) remove(path);
}
(void) remove(sf_root);
}
/* A missing asset would silently reduce the target to its parser half. */
static void sf_write(const char *name, const char *data, size_t len) {
char path[700];
FILE *fp;
snprintf(path, sizeof(path), "%s/%s", sf_root, name);
fp = fopen(path, "wb");
if (fp == NULL || fwrite(data, 1, len, fp) != len)
abort();
fclose(fp);
}
static void sf_text(const char *name, const char *data) {
sf_write(name, data, strlen(data));
}
static void sf_init(void) {
static const char png[] = "\x89PNG\r\n\x1a\n";
static const char big[4096] = "\x89PNG";
const char *tmp = getenv("TMPDIR");
char path[700];
hts_init();
snprintf(sf_root, sizeof(sf_root), "%s/httrack-fuzz-sf-XXXXXX",
tmp != NULL && tmp[0] != '\0' ? tmp : "/tmp");
if (mkdtemp(sf_root) == NULL)
abort();
atexit(sf_cleanup);
snprintf(sf_page, sizeof(sf_page), "%s/page.html", sf_root);
snprintf(path, sizeof(path), "%s/sub", sf_root);
if (mkdir(path, 0700) != 0)
abort();
sf_write("a.png", png, sizeof(png) - 1);
sf_write("big.png", big, sizeof(big));
sf_text("j.js", "var x=1;\n");
/* @import plus a url(), so an inlined stylesheet recurses and its own
relative reference is rebased. */
sf_text("s.css", "@import url(sub/b.css);\ndiv{background:url(a.png)}\n");
sf_text("sub/b.css", "p{background:url(../a.png)}\n");
}
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
static int inited = 0;
String out = STRING_EMPTY;
httrackp *opt;
/* Exact-length, unterminated: the rewriter is span-based, so ASan bounds a
read past html_len instead of it landing on a terminator. */
char *html = malloct(size != 0 ? size : 1);
if (!inited) {
sf_init();
inited = 1;
}
memcpy(html, data, size);
opt = hts_create_opt();
opt->log = opt->errlog = NULL;
opt->single_file_max_size = FUZZ_SF_CAP;
StringClear(out);
(void) singlefile_rewrite_html(opt, sf_root, sf_page, html, size,
SINGLEFILE_MAX_PAGE_SIZE, &out);
StringFree(out);
freet(html);
hts_free_opt(opt);
return 0;
}

60
fuzz/fuzz-sitemap.c Normal file
View File

@@ -0,0 +1,60 @@
/* ------------------------------------------------------------ */
/*
HTTrack Website Copier, Offline Browser for Windows and Unix
Copyright (C) 2026 Xavier Roche and other contributors
SPDX-License-Identifier: GPL-3.0-or-later
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Ethical use: we kindly ask that you NOT use this software to harvest email
addresses or to collect any other private information about people. Doing so
would dishonor our work and waste the many hours we have spent on it.
Please visit our Website: http://www.httrack.com
*/
/* Fuzz the sitemap <loc> scanner (htssitemap.c): raw XML, gzip-framed bodies
and truncated streams all arrive here straight off the network. */
#include "fuzz.h"
#include "htssitemap.h"
static hts_boolean sm_count(void *arg, const char *url) {
int *const n = (int *) arg;
(void) url;
(*n)++;
return HTS_TRUE;
}
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
static const int caps[] = {0, 1, 16, HTS_SITEMAP_MAX_URLS_DOC};
hts_boolean is_index;
char *body;
int n = 0, cap;
if (size == 0)
return 0;
cap = caps[data[0] % (sizeof(caps) / sizeof(caps[0]))];
data++, size--;
/* A heap copy of exactly `size` bytes: the scanner must never rely on a
terminator, and ASan turns any overread into a report. */
body = malloct(size != 0 ? size : 1);
memcpy(body, data, size);
(void) hts_sitemap_scan(body, size, cap, &is_index, sm_count, &n);
freet(body);
return 0;
}

263
html/changes.html Normal file
View File

@@ -0,0 +1,263 @@
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<meta name="description" content="HTTrack is an easy-to-use website mirror utility. It allows you to download a World Wide website from the Internet to a local directory,building recursively all structures, getting html, images, and other files from the server to your computer. Links are rebuiltrelatively so that you can freely browse to the local site (works with any browser). You can mirror several sites together so that you can jump from one toanother. You can, also, update an existing mirror site, or resume an interrupted download. The robot is fully configurable, with an integrated help" />
<meta name="keywords" content="httrack, HTTRACK, HTTrack, winhttrack, WINHTTRACK, WinHTTrack, offline browser, web mirror utility, aspirateur web, surf offline, web capture, www mirror utility, browse offline, local site builder, website mirroring, aspirateur www, internet grabber, capture de site web, internet tool, hors connexion, unix, dos, windows 95, windows 98, solaris, ibm580, AIX 4.0, HTS, HTGet, web aspirator, web aspirateur, libre, GPL, GNU, free software" />
<title>HTTrack Website Copier - Change report format specification</title>
<style type="text/css">
<!--
body {
margin: 0; padding: 0; margin-bottom: 15px; margin-top: 8px;
background: #77b;
}
body, td {
font: 14px "Trebuchet MS", Verdana, Arial, Helvetica, sans-serif;
}
#subTitle {
background: #000; color: #fff; padding: 4px; font-weight: bold;
}
#siteNavigation a, #siteNavigation .current {
font-weight: bold; color: #448;
}
#siteNavigation a:link { text-decoration: none; }
#siteNavigation a:visited { text-decoration: none; }
#siteNavigation .current { background-color: #ccd; }
#siteNavigation a:hover { text-decoration: none; background-color: #fff; color: #000; }
#siteNavigation a:active { text-decoration: none; background-color: #ccc; }
a:link { text-decoration: underline; color: #00f; }
a:visited { text-decoration: underline; color: #000; }
a:hover { text-decoration: underline; color: #c00; }
a:active { text-decoration: underline; }
#pageContent {
clear: both;
border-bottom: 6px solid #000;
padding: 10px; padding-top: 20px;
line-height: 1.65em;
background-image: url(images/bg_rings.gif);
background-repeat: no-repeat;
background-position: top right;
}
#pageContent, #siteNavigation {
background-color: #ccd;
}
.imgLeft { float: left; margin-right: 10px; margin-bottom: 10px; }
.imgRight { float: right; margin-left: 10px; margin-bottom: 10px; }
hr { height: 1px; color: #000; background-color: #000; margin-bottom: 15px; }
h1 { margin: 0; font-weight: bold; font-size: 2em; }
h2 { margin: 0; font-weight: bold; font-size: 1.6em; }
h3 { margin: 0; font-weight: bold; font-size: 1.3em; }
h4 { margin: 0; font-weight: bold; font-size: 1.18em; }
.blak { background-color: #000; }
.hide { display: none; }
.tableWidth { min-width: 400px; }
.tblRegular { border-collapse: collapse; }
.tblRegular td { padding: 6px; background-image: url(fade.gif); border: 2px solid #99c; }
.tblHeaderColor, .tblHeaderColor td { background: #99c; }
.tblNoBorder td { border: 0; }
// -->
</style>
</head>
<table width="76%" border="0" align="center" cellspacing="0" cellpadding="0" class="tableWidth">
<tr>
<td><img src="images/header_title_4.gif" width="400" height="34" alt="HTTrack Website Copier" title="" border="0" id="title" /></td>
</tr>
</table>
<table width="76%" border="0" align="center" cellspacing="0" cellpadding="3" class="tableWidth">
<tr>
<td id="subTitle">Open Source offline browser</td>
</tr>
</table>
<table width="76%" border="0" align="center" cellspacing="0" cellpadding="0" class="tableWidth">
<tr class="blak">
<td>
<table width="100%" border="0" align="center" cellspacing="1" cellpadding="0">
<tr>
<td colspan="6">
<table width="100%" border="0" align="center" cellspacing="0" cellpadding="10">
<tr>
<td id="pageContent">
<!-- ==================== End prologue ==================== -->
<h2 align="center"><em>Change report format specification</em></h2>
<br />
Run with <tt>--changes</tt> (<tt>-%d</tt>), HTTrack writes <tt>hts-changes.json</tt>
in the project directory, next to <tt>hts-log.txt</tt>, describing what the crawl
left new, changed, unchanged and gone compared to the previous mirror. The file is
rewritten from scratch at the end of every run, and the log carries a one-line
summary of the same counts.
<br /><br />
<h3>What "changed" means</h3>
A resource is changed when its bytes differ, not when the server merely re-sent
it. HTTrack compares the payload it just received against the copy the previous
run left behind: for pages it parses, the previous payload comes from the cache
(the file on disk carries the mirror footer and its crawl date, so its bytes
differ on every run); for everything else, the mirrored file is the payload
verbatim and is compared directly.
<br /><br />
Where no digest can be taken on either side, because the cache is disabled or
the previous copy is gone, the report falls back to the transfer signal, and a
server that answers 200 rather than 304 reads as changed. Keeping the cache on
(the default) is what makes the report precise.
<br /><br />
<h3>With the cache off</h3>
<tt>--cache=0</tt> costs the report more than the digest of a parsed page. The
mirror's file index (<tt>hts-cache/new.lst</tt>) is what records which files a
run produced, so without it there is no previous mirror to subtract from: nothing
is reported <tt>gone</tt>, and whether the run is a first crawl cannot be decided
at all, which <tt>first_crawl</tt> states as <tt>null</tt> rather than guess. What
is on disk is still compared byte for byte, so the other three lists stay
meaningful, except for the pages HTTrack parses: those have no cached payload to
compare against and fall back to the transfer signal.
<br /><br />
<h3>Fields</h3>
<ul>
<li><tt>schema</tt>: format version, currently <tt>1</tt>. It is bumped only
on an incompatible change; new fields may appear without one.</li>
<li><tt>generator</tt>: the HTTrack build that wrote the file.</li>
<li><tt>date</tt>: when the report was written, UTC, <tt>YYYY-MM-DDThh:mm:ssZ</tt>.</li>
<li><tt>first_crawl</tt>: true when no index of a previous mirror
(<tt>hts-cache/old.lst</tt>) was found, so there was nothing to compare against and
everything is listed as new. Null when the run kept no index at all and the
question cannot be answered (see above).</li>
<li><tt>partial</tt>: true when the report ran out of memory and lists only
part of the mirror.</li>
<li><tt>purged</tt>: true when <tt>--purge-old</tt> was in effect, so the
files under <tt>gone</tt> were also deleted from disk.</li>
<li><tt>counts</tt>: the size of each of the four lists.</li>
<li><tt>new</tt>, <tt>changed</tt>, <tt>unchanged</tt>, <tt>gone</tt>: the
lists themselves. Every mirrored file appears in exactly one of them.</li>
</ul>
Each entry is an object:
<ul>
<li><tt>url</tt>: the absolute URL the file came from. Empty under
<tt>gone</tt>: deletions are computed from the mirror's file index, which records
paths, not URLs.</li>
<li><tt>file</tt>: the path relative to the mirror root, with forward
slashes. This is the entry's identity: a URL and a redirect that resolve to the
same local file are one entry, not two.</li>
<li><tt>size</tt>: the mirrored file's size in bytes, absent when the file
is not on disk.</li>
<li><tt>previous_size</tt>: under <tt>changed</tt> only, the size of the
copy the previous run left.</li>
</ul>
<br />
<h3>Encoding</h3>
The file is JSON, UTF-8. URLs and local paths reach HTTrack as raw bytes and are
not guaranteed to be valid UTF-8; any byte sequence that is not becomes
U+FFFD (<tt>\ufffd</tt>), so the file always parses. Compare on <tt>file</tt>
rather than on <tt>url</tt> when a mirror is known to carry legacy-charset URLs.
<br /><br />
<h3>Example</h3>
<pre>
{
"schema": 1,
"generator": "HTTrack Website Copier/3.49-14",
"date": "2026-07-26T15:29:03Z",
"first_crawl": false,
"partial": false,
"purged": true,
"counts": { "new": 1, "changed": 1, "unchanged": 1, "gone": 1 },
"new": [
{ "url": "http://example.com/d.html", "file": "example.com/d.html", "size": 280 }
],
"changed": [
{ "url": "http://example.com/a.html", "file": "example.com/a.html", "size": 281, "previous_size": 273 }
],
"unchanged": [
{ "url": "http://example.com/b.html", "file": "example.com/b.html", "size": 277 }
],
"gone": [
{ "url": "", "file": "example.com/c.html" }
]
}
</pre>
<br /><br />
<h3>Notes</h3>
<ul>
<li>A file listed under <tt>gone</tt> is only deleted when <tt>--purge-old</tt> is
on. Left in place it drops out of the mirror's index, so it is reported once and
not again.</li>
<li>A resource whose local file name changed since the previous mirror (a new
MIME type, say) is reported as <tt>new</tt> under its new name; the old name is
reported as <tt>gone</tt> only if the file is still on disk. The two entries are
not paired.</li>
<li>A resource this run tried and failed to transfer also drops out of the
mirror's index, but its previous copy is untouched, so it is reported
<tt>unchanged</tt>. Under <tt>--purge-old</tt> that copy is deleted anyway, and
the report says <tt>gone</tt> to match.</li>
<li>A run that transfers no data at all is rolled back: HTTrack restores the
previous cache generation and leaves the previous report in place, so a lost
connection does not overwrite a good report with an empty one.</li>
<li>Content diffs, and keeping the previous copy of a changed page, are out of
scope: both change what a mirror directory contains.</li>
</ul>
<br /><br />
<!-- ==================== Start epilogue ==================== -->
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
<table width="76%" border="0" align="center" valign="bottom" cellspacing="0" cellpadding="0">
<tr>
<td id="footer"><small>&copy; 1998-2026 Xavier Roche & other contributors - Web Design: Leto Kauler.</small></td>
</tr>
</table>
</body>
</html>

View File

@@ -163,8 +163,26 @@ the index" problems disappear.</p>
<tr><td><tt>--near (-n)</tt></td><td>Also fetch non-HTML files "near" a followed link, such as an image linked from a page you kept but hosted elsewhere.</td></tr>
<tr><td><tt>--ext-depth (-%e)</tt></td><td>How many levels of external links to follow once the crawl leaves your scope (default 0).</td></tr>
<tr><td><tt>--test (-t)</tt></td><td>Also HEAD-test links that fall outside the scope, which are normally refused, without downloading them: a way to see what scope is excluding.</td></tr>
<tr><td><tt>--sitemap (-%m), --sitemap-url URL (-%mu)</tt></td><td>Also take start URLs from the site's sitemap, for pages nothing links to. Off by default.</td></tr>
</table>
<p>Link-following only finds what something links to. Anything a site publishes
solely in its sitemap is invisible to HTTrack unless you ask for it.
<tt>--sitemap</tt> reads the start host's <tt>robots.txt</tt> for
<tt>Sitemap:</tt> lines and falls back to <tt>/sitemap.xml</tt>;
<tt>--sitemap-url</tt> names one directly. Nested <tt>sitemapindex</tt> files
and gzipped <tt>.xml.gz</tt> sitemaps are followed. The URLs found become start
URLs with the full depth budget, but they still go through your filters and
scope rules, so a sitemap cannot widen a crawl you deliberately narrowed. It is
off by default because a sitemap can list thousands of pages nothing links
to.</p>
<p>One surprise worth knowing: a sitemap you name with <tt>--sitemap-url</tt>,
and one the site itself declares in <tt>robots.txt</tt>, are fetched even when
<tt>robots.txt</tt> disallows that path, because naming or declaring a sitemap
is an invitation to read it. Only the guessed <tt>/sitemap.xml</tt> obeys a
<tt>Disallow</tt>. The URLs listed inside are gated normally either way.</p>
<p>The single most common surprise is "only the home page came down." That is
usually not a scope option at all: it is an off-host redirect. A start URL of
<tt>http://example.com/</tt> that redirects to <tt>https://www.example.com/</tt>
@@ -300,6 +318,7 @@ ones it kept so the local copy browses offline. These options tune both halves.<
<tr><td><tt>--preserve (-%p), --disable-passwords (-%x)</tt></td><td>Leave HTML untouched (no rewriting), and strip passwords out of saved links.</td></tr>
<tr><td><tt>--extended-parsing (-%P), --parse-java (-j)</tt></td><td>Aggressive link discovery, and how much script content is parsed for links.</td></tr>
<tr><td><tt>--mime-html (-%M)</tt></td><td>Save the whole mirror as a single MIME-encapsulated <tt>.mht</tt> archive (<tt>index.mht</tt>).</td></tr>
<tr><td><tt>--single-file (-%Z), --single-file-max-size N</tt></td><td>Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded as <tt>data:</tt> URIs. Assets over the cap (10&nbsp;MB by default) keep their link, as do audio, video, and the links from one page to another. A sibling of <tt>-%M</tt>, not a replacement: see the recipe below for which to pick.</td></tr>
<tr><td><tt>--index (-I), --build-top-index (-%i), --search-index (-%I)</tt></td><td>Build a per-mirror index, a top index across projects, and a searchable keyword index.</td></tr>
</table>
@@ -482,6 +501,34 @@ add <tt>--warc-cdx</tt> for a sorted CDXJ index, or <tt>--wacz</tt> to bundle th
archive, index and pages into one WACZ for replay tools such as
replayweb.page.</small></p>
<h4>See what a re-crawl changed</h4>
<p><tt>httrack https://example.com/ --update --changes --path mydir</tt><br>
<small>Writes <tt>hts-changes.json</tt> in the project folder, listing every
mirrored file as new, changed, unchanged or gone, plus a one-line summary in the
log. &quot;Changed&quot; means the bytes really differ: a server that answers 200
with the same content it served last time lands in <tt>unchanged</tt>. Deletions
are reported whether or not <tt>--purge-old</tt> is deleting them. The format is
documented in <a href="changes.html">the change report specification</a>.</small></p>
<h4>Pages you can hand to someone as one file</h4>
<p><tt>httrack https://example.com/ --single-file --path mydir</tt><br>
<small>Rewrites each saved page after the crawl so its stylesheets, scripts,
images and fonts are embedded as <tt>data:</tt> URIs. The mirror stays a normal
browsable tree, with links between pages relative and the assets still on disk,
but any single <tt>.html</tt> file can now be mailed or archived on its own.
Raise or lower the 10&nbsp;MB per-asset limit with
<tt>--single-file-max-size N</tt>; anything over it, plus audio and video, keeps
its link. One caveat if you raise it: an inlined stylesheet becomes a
<tt>data:</tt> URL, whose path is opaque, so an asset it referenced that stayed
over the cap no longer resolves from inside it. Raising the cap past that asset
embeds it too and the question goes away.<br>
Reach for this when the file has to open for someone you cannot make assumptions
about: it is plain HTML and needs no add-on. Reach for <tt>--mime-html</tt>
instead when a Chromium-family browser is a given and the mirror is large: MIME
carries text parts without the base64 tax, keeps each resource's original URL,
and stores a shared asset once rather than re-embedding it in every page that
uses it.</small></p>
<h4>HTTrack as a fetch tool</h4>
<p><tt>httrack --get https://host/file.bin --path tmp</tt><br>
<small><tt>--get</tt> fetches one file with cache, index, depth, cookies and robots
@@ -490,7 +537,8 @@ all disabled.</small></p>
<p><br>For the complete, always-current option list, see
<a href="httrack.man.html">the manual page</a>. For the filter language, see
<a href="filters.html">filters</a>; for the cache and updates, see
<a href="cache.html">cache</a>.</p>
<a href="cache.html">cache</a>; for the change report, see
<a href="changes.html">changes</a>.</p>
<!-- ==================== Start epilogue ==================== -->
</td>

View File

@@ -129,6 +129,9 @@ The library can be used to write graphical GUIs for httrack, or to run mirrors f
<li><a href="cache.html">Cache format</a></li><br>
HTTrack stores original HTML data and references to downloaded files in a cache, located in the hts-cache directory.
This page describes the HTTrack cache format.
<li><a href="changes.html">Change report format</a></li><br>
With --changes, HTTrack writes hts-changes.json describing what the crawl left new, changed, unchanged and gone
compared to the previous mirror. This page describes that file.
</ul>

View File

@@ -237,7 +237,7 @@ Build options:
x replace external html links by error pages (--replace-external)
%x do not include any password for external password protected websites (%x0 include) (--no-passwords)
%q *include query string for local files (useless, for information purpose only) (%q0 don't include) (--include-query-string)
o *generate output html file in case of error (404..) (o0 don't generate) (--generate-errors)
o *save the server's error pages (404..) (o0 discard them) (--generate-errors)
X *purge old files after update (X0 keep delete) (--purge-old[=N])
Spider options:
@@ -413,7 +413,7 @@ site. Specifically, the defauls are:
NN name conversion type (0 *original structure, 1+: see below)
LN long names (L1 *long names / L0 8-3 conversion)
K keep original links (e.g. http://www.adr/link) (K0 *relative link)
o *generate output html file in case of error (404..) (o0 don't generate)
o *save the server's error pages (404..) (o0 discard them)
X *purge old files after update (X0 keep delete)
bN accept cookies in cookies.txt (0=do not accept,* 1=accept)
u check document type if unknown (cgi,asp..) (u0 don't check, * u1 check but /, u2 check always)
@@ -473,11 +473,11 @@ store them with the same names used on the web site.
URLs within this web site are adjusted to point to the files in the
mirror.
<pre><b><i> o *generate output html file in case of error (404..) (o0 don't generate) </i></b></pre>
<pre><b><i> o *save the server's error pages (404..) (o0 discard them) </i></b></pre>
<p align=justify> IF there are errors in downloading, create a file that
indicates that the URL was not found. This makes browsing go a lot
smoother.
<p align=justify> IF a page cannot be downloaded, the error page the
server sent is saved in its place, so a broken link still lands on the
site's own 'not found' page. This makes browsing go a lot smoother.
<pre><b><i> X *purge old files after update (X0 keep delete) </i></b></pre>
@@ -1011,7 +1011,7 @@ Build options:
LN long names (L1 *long names / L0 8-3 conversion)
K keep original links (e.g. http://www.adr/link) (K0 *relative link)
x replace external html links by error pages
o *generate output html file in case of error (404..) (o0 don't generate)
o *save the server's error pages (404..) (o0 discard them)
X *purge old files after update (X0 keep delete)
%x do not include any password for external password protected websites (%x0 include) (--no-passwords)
%q *include query string for local files (information only) (%q0 don't include) (--include-query-string)
@@ -1118,9 +1118,9 @@ deactivated byt his process.
httrack http://www.shoesizes.com -O /tmp/shoesizes -x
</i></b></pre>
<p align=justify> This option prevents the generation of '404' error
files to replace files that were not found even though there were URLs
pointing to them. It is useful for saving space as well as eliminating
<p align=justify> This option keeps the server's '404' error pages out
of the mirror, even though there were URLs pointing to the missing
files. It is useful for saving space as well as eliminating
unnecessary files in operations where a working web site is not the
desired result.

View File

@@ -87,12 +87,13 @@ offline browser : copy websites to a local directory</p>
--host-control[=N]</b> ] [ <b>-%P,
--extended-parsing[=N]</b> ] [ <b>-n, --near</b> ] [ <b>-t,
--test</b> ] [ <b>-%L, --list</b> ] [ <b>-%S, --urllist</b>
] [ <b>-NN, --structure[=N]</b> ] [ <b>-%N,
--delayed-type-check</b> ] [ <b>-%D,
] [ <b>-%m, --sitemap</b> ] [ <b>-NN, --structure[=N]</b> ]
[ <b>-%N, --delayed-type-check</b> ] [ <b>-%D,
--cached-delayed-type-check</b> ] [ <b>-%M, --mime-html</b>
] [ <b>-LN, --long-names[=N]</b> ] [ <b>-KN,
--keep-links[=N]</b> ] [ <b>-x, --replace-external</b> ] [
<b>-%x, --disable-passwords</b> ] [ <b>-%q,
] [ <b>-%Z, --single-file</b> ] [ <b>-LN,
--long-names[=N]</b> ] [ <b>-KN, --keep-links[=N]</b> ] [
<b>-x, --replace-external</b> ] [ <b>-%x,
--disable-passwords</b> ] [ <b>-%q,
--include-query-string</b> ] [ <b>-%g, --strip-query</b> ] [
<b>-o, --generate-errors</b> ] [ <b>-X, --purge-old[=N]</b>
] [ <b>-%p, --preserve</b> ] [ <b>-%T, --utf8-conversion</b>
@@ -108,16 +109,17 @@ offline browser : copy websites to a local directory</p>
--footer</b> ] [ <b>-%l, --language</b> ] [ <b>-%a,
--accept</b> ] [ <b>-%X, --headers</b> ] [ <b>-C,
--cache[=N]</b> ] [ <b>-k, --store-all-in-cache</b> ] [
<b>-%r, --warc</b> ] [ <b>-%n, --do-not-recatch</b> ] [
<b>-%v, --display</b> ] [ <b>-Q, --do-not-log</b> ] [ <b>-q,
--quiet</b> ] [ <b>-z, --extra-log</b> ] [ <b>-Z,
--debug-log</b> ] [ <b>-v, --verbose</b> ] [ <b>-f,
--file-log</b> ] [ <b>-f2, --single-log</b> ] [ <b>-I,
--index</b> ] [ <b>-%i, --build-top-index</b> ] [ <b>-%I,
--search-index</b> ] [ <b>-pN, --priority[=N]</b> ] [ <b>-S,
--stay-on-same-dir</b> ] [ <b>-D, --can-go-down</b> ] [
<b>-U, --can-go-up</b> ] [ <b>-B, --can-go-up-and-down</b> ]
[ <b>-a, --stay-on-same-address</b> ] [ <b>-d,
<b>-%r, --warc</b> ] [ <b>-%d, --changes</b> ] [ <b>-%n,
--do-not-recatch</b> ] [ <b>-%v, --display</b> ] [ <b>-Q,
--do-not-log</b> ] [ <b>-q, --quiet</b> ] [ <b>-z,
--extra-log</b> ] [ <b>-Z, --debug-log</b> ] [ <b>-v,
--verbose</b> ] [ <b>-f, --file-log</b> ] [ <b>-f2,
--single-log</b> ] [ <b>-I, --index</b> ] [ <b>-%i,
--build-top-index</b> ] [ <b>-%I, --search-index</b> ] [
<b>-pN, --priority[=N]</b> ] [ <b>-S, --stay-on-same-dir</b>
] [ <b>-D, --can-go-down</b> ] [ <b>-U, --can-go-up</b> ] [
<b>-B, --can-go-up-and-down</b> ] [ <b>-a,
--stay-on-same-address</b> ] [ <b>-d,
--stay-on-same-domain</b> ] [ <b>-l, --stay-on-same-tld</b>
] [ <b>-e, --go-everywhere</b> ] [ <b>-%H,
--debug-headers</b> ] [ <b>-%!,
@@ -575,6 +577,22 @@ URL per line) (--list &lt;param&gt;)</p></td></tr>
<p>&lt;file&gt; add all scan rules located in this text
file (one scan rule per line) (--urllist &lt;param&gt;)</p></td></tr>
<tr valign="top" align="left">
<td width="9%"></td>
<td width="4%">
<p>-%m</p></td>
<td width="5%"></td>
<td width="82%">
<p>seed the crawl from the site&rsquo;s sitemap (robots.txt
Sitemap:, then /sitemap.xml); --sitemap-url URL names one
explicitly. A sitemap you name, or one the site declares, is
fetched even under robots.txt Disallow; only the guessed
/sitemap.xml obeys it. The URLs found still pass every
filter and scope rule (--sitemap)</p></td></tr>
</table>
<h3>Build options:
@@ -648,6 +666,24 @@ don&rsquo;t wait) (--cached-delayed-type-check)</p></td></tr>
<td width="4%">
<p>-%Z</p></td>
<td width="5%"></td>
<td width="82%">
<p>after the mirror, rewrite each saved page with its
stylesheets, scripts, images and fonts inlined as data:
URIs, so any page opens by double-click anywhere (links
between pages stay relative; audio and video stay links);
--single-file-max-size N caps each asset (default 10485760
bytes). %M is the better container where a Chromium-family
browser is a given: one archive, no base64 tax on text, a
shared asset stored once (--single-file)</p></td></tr>
<tr valign="top" align="left">
<td width="9%"></td>
<td width="4%">
<p>-%t</p></td>
<td width="5%"></td>
<td width="82%">
@@ -741,8 +777,8 @@ information purpose only) (%q0 don&rsquo;t include)
<td width="82%">
<p>*generate output html file in case of error (404..) (o0
don&rsquo;t generate) (--generate-errors)</p></td></tr>
<p>*save the server&rsquo;s error pages (404..) (o0 discard
them) (--generate-errors)</p></td></tr>
<tr valign="top" align="left">
<td width="9%"></td>
<td width="4%">
@@ -1143,6 +1179,19 @@ past N bytes, --warc-cdx also writes a sorted CDXJ index,
<td width="4%">
<p>-%d</p></td>
<td width="5%"></td>
<td width="82%">
<p>write hts-changes.json listing what this crawl left new,
changed, unchanged and gone compared to the previous mirror
(--changes)</p> </td></tr>
<tr valign="top" align="left">
<td width="9%"></td>
<td width="4%">
<p>-%n</p></td>
<td width="5%"></td>
<td width="82%">

View File

@@ -97,9 +97,8 @@ ${do:end-if}
</pre>
${LANG_G8} :
${do:output-mode:html-urlescaped}
<a href="file://${path}/${projname}/" target="_new">
${do:output-mode:}
${/* an http: page cannot navigate to file:, so the mirror is reached through the server */}
<a href="/website/index.html" target="_new">
${path}/${projname}
</a></li>
<ul>

View File

@@ -98,6 +98,9 @@ ${do:end-if}
<input type="hidden" name="redirect" value="">
<input type="hidden" name="closeme" value="">
<!-- clear if not checked -->
<input type="hidden" name="ftpprox" value="">
${LANG_PROXYTYPE}
<select name="proxytype"
title='${html:LANG_PROXYTYPETIP}' onMouseOver="info('${html:LANG_PROXYTYPETIP}'); return true" onMouseOut="info('&nbsp;'); return true"

View File

@@ -98,6 +98,14 @@ ${do:end-if}
<input type="hidden" name="redirect" value="">
<input type="hidden" name="closeme" value="">
<!-- clear if not checked -->
<input type="hidden" name="errpage" value="">
<input type="hidden" name="external" value="">
<input type="hidden" name="hidepwd" value="">
<input type="hidden" name="hidequery" value="">
<input type="hidden" name="nopurge" value="">
<input type="hidden" name="singlefile" value="">
${LANG_I33}
<br>
<select name="build"
@@ -136,6 +144,13 @@ ${listid:build:LISTDEF_3}
<tr><td><input type="checkbox" name="nopurge" ${checked:nopurge}
title='${html:LANG_I1a}' onMouseOver="info('${html:LANG_I1a}'); return true" onMouseOut="info('&nbsp;'); return true"
> ${LANG_I57}</td></tr>
<tr><td><input type="checkbox" name="singlefile" ${checked:singlefile}
title='${html:LANG_SINGLEFILETIP}' onMouseOver="info('${html:LANG_SINGLEFILETIP}'); return true" onMouseOut="info('&nbsp;'); return true"
> ${LANG_SINGLEFILE}</td></tr>
<tr><td>${LANG_SINGLEFILEMAX}
<input name="singlefilemax" value="${singlefilemax}" size="12"
title='${html:LANG_SINGLEFILEMAXTIP}' onMouseOver="info('${html:LANG_SINGLEFILEMAXTIP}'); return true" onMouseOut="info('&nbsp;'); return true"
></td></tr>
</table>
<tr><td>

View File

@@ -103,7 +103,7 @@ ${LANG_Q3}
<tr><td>
<table width="100%">
<tr><td align="left">
<input type="submit" value="${LANG_OK]"
<input type="submit" value="${LANG_OK}"
${do:output-mode:html-urlescaped}
onClick="if (confirm(str_replace(str_replace('${LANG_DIAL7}', '%20', ' '), '%0a', ' '))) { form.closeme.value=1; form.submit(); } return false;"
${do:output-mode:}

View File

@@ -98,6 +98,9 @@ ${do:end-if}
<input type="hidden" name="redirect" value="">
<input type="hidden" name="closeme" value="">
<!-- clear if not checked -->
<input type="hidden" name="windebug" value="">
${LANG_I40c}
<br>

View File

@@ -98,6 +98,11 @@ ${do:end-if}
<input type="hidden" name="redirect" value="">
<input type="hidden" name="closeme" value="">
<!-- clear if not checked -->
<input type="hidden" name="ka" value="">
<input type="hidden" name="remt" value="">
<input type="hidden" name="rems" value="">
<table border="0" width="100%" cellspacing="0">
<tr><td>

View File

@@ -98,6 +98,18 @@ ${do:end-if}
<input type="hidden" name="redirect" value="">
<input type="hidden" name="closeme" value="">
<!-- clear if not checked -->
<input type="hidden" name="cookies" value="">
<input type="hidden" name="parsejava" value="">
<input type="hidden" name="updhack" value="">
<input type="hidden" name="urlhack" value="">
<input type="hidden" name="keepwww" value="">
<input type="hidden" name="keepslashes" value="">
<input type="hidden" name="keepqueryorder" value="">
<input type="hidden" name="toler" value="">
<input type="hidden" name="http10" value="">
<input type="hidden" name="sitemap" value="">
<input type="checkbox" name="cookies" ${checked:cookies}
title='${html:LANG_I1b}' onMouseOver="info('${html:LANG_I1b}'); return true" onMouseOut="info('&nbsp;'); return true"
> ${LANG_I58}
@@ -132,6 +144,17 @@ ${listid:robots:LISTDEF_8}
</select>
<br><br>
<input type="checkbox" name="sitemap" ${checked:sitemap}
title='${html:LANG_SITEMAPTIP}' onMouseOver="info('${html:LANG_SITEMAPTIP}'); return true" onMouseOut="info('&nbsp;'); return true"
> ${LANG_SITEMAP}
<br><br>
${LANG_SITEMAPURL}
<input name="sitemapurl" value="${sitemapurl}" size="40"
title='${html:LANG_SITEMAPURLTIP}' onMouseOver="info('${html:LANG_SITEMAPURLTIP}'); return true" onMouseOut="info('&nbsp;'); return true"
>
<br><br>
<input type="checkbox" name="updhack" ${checked:updhack}
title='${html:LANG_I1k}' onMouseOver="info('${html:LANG_I1k}'); return true" onMouseOut="info('&nbsp;'); return true"
> ${LANG_I62b}

View File

@@ -98,6 +98,14 @@ ${do:end-if}
<input type="hidden" name="redirect" value="">
<input type="hidden" name="closeme" value="">
<!-- clear if not checked -->
<input type="hidden" name="warc" value="">
<input type="hidden" name="changes" value="">
<input type="hidden" name="norecatch" value="">
<input type="hidden" name="logf" value="">
<input type="hidden" name="index" value="">
<input type="hidden" name="index2" value="">
<input type="checkbox" name="cache2" ${checked:cache2}
title='${html:LANG_I1e}' onMouseOver="info('${html:LANG_I1e}'); return true" onMouseOut="info('&nbsp;'); return true"
> ${LANG_I61}
@@ -114,6 +122,11 @@ ${LANG_WARCFILE}
>
<br><br>
<input type="checkbox" name="changes" ${checked:changes}
title='${html:LANG_CHANGESTIP}' onMouseOver="info('${html:LANG_CHANGESTIP}'); return true" onMouseOut="info('&nbsp;'); return true"
> ${LANG_CHANGES}
<br><br>
<input type="checkbox" name="norecatch" ${checked:norecatch}
title='${html:LANG_I5b}' onMouseOver="info('${html:LANG_I5b}'); return true" onMouseOut="info('&nbsp;'); return true"
> ${LANG_I34b}

View File

@@ -141,8 +141,13 @@ ${do:copy:KeepSlashes:keepslashes}
${do:copy:KeepQueryOrder:keepqueryorder}
${do:copy:StripQuery:stripquery}
${do:copy:StoreAllInCache:cache2}
${do:copy:Sitemap:sitemap}
${do:copy:SitemapUrl:sitemapurl}
${do:copy:Warc:warc}
${do:copy:WarcFile:warcfile}
${do:copy:Changes:changes}
${do:copy:SingleFile:singlefile}
${do:copy:SingleFileMaxSize:singlefilemax}
${do:copy:LogType:logtype}
${do:copy:UseHTTPProxyForFTP:ftpprox}
${do:copy:ProxyType:proxytype}

View File

@@ -114,16 +114,16 @@ ${do:end-if}
${/* Real commands and ini file generated below */}
<!-- engine commandline -->
<!-- engine commandline; ztest so a cleared default-on option still emits its disabling flag -->
${do:output-mode:html}
<textarea name="command" cols="50" rows="4" style="visibility:hidden">
httrack \
--quiet \
--build-top-index \
${test:todo:--mirror:--mirror:--mirror-wizard:--get:--mirrorlinks:--testlinks:--continue:--update}
${urls}
${test:filelist:-%L "}${filelist}${test:filelist:"}
--path "${html:path}/${html:projname}"
${unquoted:urls}
${test:filelist:-%L "}${arg:filelist}${test:filelist:"}
--path "${arg:path}/${arg:projname}"
\
${test:parseall:--near}
${test:link:--test}
@@ -131,7 +131,7 @@ httrack \
${test:htmlfirst::--priority=7}
\
${do:if-not-empty:BuildString}
--structure "${BuildString}"
--structure "${arg:BuildString}"
${do:end-if}
${test:build:-N0:-N0:-N1:-N2:-N3:-N4:-N5:-N100:-N101:-N102:-N103:-N104:-N105:-N99:-N199:}
\
@@ -150,51 +150,57 @@ ${do:end-if}
${test:travel3::--keep-links=0:--keep-links:--keep-links=3:--keep-links=4}
${test:windebug:--debug-headers}
\
${test:connexion:--sockets=}${connexion}
${test:connexion:--sockets=}${unquoted:connexion}
${test:ka:--keep-alive}
${test:timeout:--timeout=}${timeout}
${test:timeout:--timeout=}${unquoted:timeout}
${test:remt:--host-control=1}
${test:retry:--retries=}${retry}
${test:rate:--min-rate=}${rate}
${test:retry:--retries=}${unquoted:retry}
${test:rate:--min-rate=}${unquoted:rate}
${test:rems:--host-control=2}
\
${test:depth:--depth=}${depth}
${test:depth2:--ext-depth=}${depth2}
${test:maxhtml:--max-files=,}${maxhtml}
${test:othermax:--max-files=}${othermax}
${test:sizemax:--max-files=}${sizemax}
${test:pausebytes:--max-pause=}${pausebytes}
${test:maxtime:--max-time=}${maxtime}
${test:maxrate:--max-rate=}${maxrate}
${test:maxconn:--connection-per-second=}${maxconn}
${test:maxlinks:--advanced-maxlinks=}${maxlinks}
${test:depth:--depth=}${unquoted:depth}
${test:depth2:--ext-depth=}${unquoted:depth2}
${/* -m<n> resets the html limit, so the bare form must precede the -m,<n> one */}
${test:othermax:--max-files=}${unquoted:othermax}
${test:maxhtml:--max-files=,}${unquoted:maxhtml}
${test:sizemax:--max-size=}${unquoted:sizemax}
${test:pausebytes:--max-pause=}${unquoted:pausebytes}
${test:maxtime:--max-time=}${unquoted:maxtime}
${test:maxrate:--max-rate=}${unquoted:maxrate}
${test:maxconn:--connection-per-second=}${unquoted:maxconn}
${test:maxlinks:--advanced-maxlinks=}${unquoted:maxlinks}
\
--user-agent "${html:user}"
--footer "${html:footer}"
--user-agent "${arg:user}"
--footer "${arg:footer}"
\
${url2}
${unquoted:url2}
\
${test:cookies:--cookies=0:}
${test:parsejava:--parse-java=0:}
${ztest:cookies:--cookies=0:}
${ztest:parsejava:--parse-java=0:}
${test:updhack:--updatehack}
${test:urlhack:--urlhack=0:--urlhack}
${test:keepwww:--keep-www-prefix}
${test:keepslashes:--keep-double-slashes}
${test:keepqueryorder:--keep-query-order}
${test:cookiesfile:--cookies-file "}${html:cookiesfile}${test:cookiesfile:"}
${test:pausefiles:--pause "}${pausefiles}${test:pausefiles:"}
${test:stripquery:--strip-query "}${html:stripquery}${test:stripquery:"}
${test:cookiesfile:--cookies-file "}${arg:cookiesfile}${test:cookiesfile:"}
${test:pausefiles:--pause "}${arg:pausefiles}${test:pausefiles:"}
${test:stripquery:--strip-query "}${arg:stripquery}${test:stripquery:"}
${test:toler:--tolerant}
${test:http10:--http-10}
${test:cache2:--store-all-in-cache}
${test:sitemap:--sitemap}
${test:sitemapurl:--sitemap-url "}${html:sitemapurl}${test:sitemapurl:"}
${test:warc:--warc}
${test:warcfile:--warc-file "}${html:warcfile}${test:warcfile:"}
${test:warcfile:--warc-file "}${arg:warcfile}${test:warcfile:"}
${test:changes:--changes}
${test:singlefile:--single-file}
${test:singlefilemax:--single-file-max-size=}${unquoted:singlefilemax}
${test:norecatch:--do-not-recatch}
${test:logf:--single-log}
${test:logtype:::--extra-log:--debug-log}
${test:index:--index=0:}
${test:index2:--search-index=0:--search-index}
${test:prox:--proxy "}${do:if-not-empty:prox}${test:proxytype::socks5:connect}${test:proxytype:\3A//}${do:end-if}${do:output-mode:html}${prox}${test:prox:\3A}${portprox}${test:prox:"}
${test:prox:--proxy "}${do:if-not-empty:prox}${test:proxytype::socks5:connect}${test:proxytype:\3A//}${do:end-if}${do:output-mode:html}${arg:prox}${test:prox:\3A}${arg:portprox}${test:prox:"}
${test:ftpprox:--httpproxy-ftp=0:--httpproxy-ftp}
</textarea>
@@ -213,7 +219,7 @@ ParseAll=${ztest:parseall:0:1}
HTMLFirst=${ztest:htmlfirst:0:1}
Cache=${ztest:cache:0:1}
NoRecatch=${ztest:norecatch:0:1}
Dos=${dos
Dos=${dos}
Index=${ztest:index:0:1}
WordIndex=${ztest:index2:0:1}
Log=${ztest:logf:0:1:2}
@@ -239,8 +245,13 @@ KeepSlashes=${ztest:keepslashes:0:1}
KeepQueryOrder=${ztest:keepqueryorder:0:1}
StripQuery=${stripquery}
StoreAllInCache=${ztest:cache2:0:1}
Sitemap=${ztest:sitemap:0:1}
SitemapUrl=${sitemapurl}
Warc=${ztest:warc:0:1}
WarcFile=${warcfile}
Changes=${ztest:changes:0:1}
SingleFile=${ztest:singlefile:0:1}
SingleFileMaxSize=${singlefilemax}
LogType=${logtype}
UseHTTPProxyForFTP=${ztest:ftpprox:0:1}
ProxyType=${proxytype}

View File

@@ -122,7 +122,7 @@ h4 { margin: 0; font-weight: bold; font-size: 1.18em; }
</small><br><br>
<!-- -->
<li>No error pages</li>
<br><small>Do not generate error pages (if a 404 error occurred, for example)
<br><small>Do not save the error pages sent by the server (if a 404 error occurred, for example)
<br>If a page is missing on the remote site, there will not be any warning on the local site
</small><br><br>
<!-- -->

View File

@@ -1042,3 +1042,23 @@ LANG_WARCFILE
WARC archive name:
LANG_WARCFILETIP
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
LANG_CHANGES
Report what changed since the previous mirror
LANG_CHANGESTIP
Also write hts-changes.json listing what this crawl left new, changed, unchanged and gone compared to the previous mirror.
LANG_SINGLEFILE
Inline assets as data: URIs (self-contained pages)
LANG_SINGLEFILETIP
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
LANG_SINGLEFILEMAX
Largest inlined asset (bytes):
LANG_SINGLEFILEMAXTIP
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
LANG_SITEMAP
Seed the crawl from the site's sitemap
LANG_SITEMAPTIP
Read the site's sitemap (robots.txt Sitemap: lines, then /sitemap.xml) and add every URL it lists as a start URL.
LANG_SITEMAPURL
Sitemap address:
LANG_SITEMAPURLTIP
Address of a sitemap to read instead of probing the site; leave blank to probe robots.txt then /sitemap.xml.

View File

@@ -964,3 +964,23 @@ WARC archive name:
Èìå íà WARC àðõèâà:
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
Íåçàäúëæèòåëíî áàçîâî èìå çà WARC àðõèâà; îñòàâåòå ïðàçíî çà àâòîìàòè÷íî èìåíóâàíå â èçõîäíàòà äèðåêòîðèÿ.
Report what changed since the previous mirror
Îò÷åò çà ïðîìåíèòå ñïðÿìî ïðåäèøíîòî îãëåäàëî
Also write hts-changes.json listing what this crawl left new, changed, unchanged and gone compared to the previous mirror.
Çàïèñâàíå è íà hts-changes.json ñúñ ñïèñúê íà íîâèòå, ïðîìåíåíèòå, íåïðîìåíåíèòå è èç÷åçíàëèòå ôàéëîâå ñïðÿìî ïðåäèøíîòî îãëåäàëî.
Inline assets as data: URIs (self-contained pages)
Âãðàæäàíå íà ðåñóðñèòå êàòî data: URI (ñàìîñòîÿòåëíè ñòðàíèöè)
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
Ñëåä çàâúðøâàíå íà îãëåäàëîòî âñÿêà çàïàçåíà ñòðàíèöà ñå ïðåçàïèñâà ñ âãðàäåíè ñòèëîâå, ñêðèïòîâå, èçîáðàæåíèÿ è øðèôòîâå, òàêà ÷å ñòðàíèöàòà äà ìîæå äà ñå îòâîðè è ñàìîñòîÿòåëíî.
Largest inlined asset (bytes):
Íàé-ãîëÿì âãðàäåí ðåñóðñ (áàéòîâå):
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
Ðåñóðñ íàä òîçè ðàçìåð çàïàçâà îáèêíîâåíà âðúçêà; îñòàâåòå ïðàçíî çà ñòîéíîñòòà ïî ïîäðàçáèðàíå îò 10485760 áàéòà.
Seed the crawl from the site's sitemap
Çàïî÷âàíå íà îáõîæäàíåòî îò êàðòàòà íà ñàéòà
Read the site's sitemap (robots.txt Sitemap: lines, then /sitemap.xml) and add every URL it lists as a start URL.
Ïðî÷èòàíå íà êàðòàòà íà ñàéòà (ðåäîâåòå Sitemap: â robots.txt, ñëåä òîâà /sitemap.xml) è äîáàâÿíå íà âñåêè ïîñî÷åí URL àäðåñ êàòî íà÷àëåí.
Sitemap address:
Àäðåñ íà êàðòàòà íà ñàéòà:
Address of a sitemap to read instead of probing the site; leave blank to probe robots.txt then /sitemap.xml.
Àäðåñ íà êàðòà íà ñàéòà, êîÿòî äà áúäå ïðî÷åòåíà âìåñòî ñîíäèðàíå íà ñàéòà; îñòàâåòå ïðàçíî, çà äà ñå ïðîâåðè robots.txt, ñëåä òîâà /sitemap.xml.

View File

@@ -964,3 +964,23 @@ WARC archive name:
Nombre del archivo WARC:
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
Nombre base opcional para el archivo WARC; déjelo en blanco para nombrarlo automáticamente en el directorio de salida.
Report what changed since the previous mirror
Informar de los cambios desde la copia anterior
Also write hts-changes.json listing what this crawl left new, changed, unchanged and gone compared to the previous mirror.
Escribir también hts-changes.json con la lista de lo que esta captura deja como nuevo, modificado, sin cambios o desaparecido respecto a la copia anterior.
Inline assets as data: URIs (self-contained pages)
Incrustar los recursos como URI data: (páginas autónomas)
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
Una vez terminada la copia, reescribir cada página guardada con sus hojas de estilo, scripts, imágenes y fuentes incrustadas, de modo que una página también pueda abrirse por sí sola.
Largest inlined asset (bytes):
Tamaño máximo del recurso incrustado (bytes):
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
Un recurso mayor que este tamaño conserva un enlace normal; déjelo vacío para el valor predeterminado de 10485760 bytes.
Seed the crawl from the site's sitemap
Iniciar el rastreo desde el mapa del sitio
Read the site's sitemap (robots.txt Sitemap: lines, then /sitemap.xml) and add every URL it lists as a start URL.
Leer el mapa del sitio (líneas Sitemap: de robots.txt, luego /sitemap.xml) y añadir como dirección inicial cada URL que incluya.
Sitemap address:
Dirección del mapa del sitio:
Address of a sitemap to read instead of probing the site; leave blank to probe robots.txt then /sitemap.xml.
Dirección de un mapa del sitio que leer en lugar de sondear el sitio; déjelo vacío para sondear robots.txt y luego /sitemap.xml.

View File

@@ -964,3 +964,23 @@ WARC archive name:
Název archivu WARC:
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
Volitelný základní název archivu WARC; ponechte prázdné pro automatické pojmenování ve výstupním adresáøi.
Report what changed since the previous mirror
Hlásit, co se od pøedchozího zrcadlení zmìnilo
Also write hts-changes.json listing what this crawl left new, changed, unchanged and gone compared to the previous mirror.
Zapsat také hts-changes.json se seznamem toho, co je oproti pøedchozímu zrcadlení nové, zmìnìné, nezmìnìné nebo chybìjící.
Inline assets as data: URIs (self-contained pages)
Vložit zdroje jako URI data: (samostatné stránky)
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
Po dokonèení zrcadlení pøepsat každou uloženou stránku s vloženými styly, skripty, obrázky a písmy, aby ji bylo možné otevøít i samostatnì.
Largest inlined asset (bytes):
Nejvìtší vložený zdroj (bajty):
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
Zdroj vìtší než tato velikost si ponechá bìžný odkaz; ponechte prázdné pro výchozí hodnotu 10485760 bajtù.
Seed the crawl from the site's sitemap
Zahájit procházení z mapy webu
Read the site's sitemap (robots.txt Sitemap: lines, then /sitemap.xml) and add every URL it lists as a start URL.
Naèíst mapu webu (øádky Sitemap: v souboru robots.txt, poté /sitemap.xml) a pøidat každou uvedenou adresu URL jako výchozí.
Sitemap address:
Adresa mapy webu:
Address of a sitemap to read instead of probing the site; leave blank to probe robots.txt then /sitemap.xml.
Adresa mapy webu, která se má naèíst místo zjiš<69>ování na webu; ponechte prázdné pro zjištìní z robots.txt a poté /sitemap.xml.

View File

@@ -964,3 +964,23 @@ WARC archive name:
WARC 封存檔名稱:
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
WARC 封存檔的選用基本名稱;留空則於輸出目錄中自動命名。
Report what changed since the previous mirror
回報自上次鏡射以來的變更
Also write hts-changes.json listing what this crawl left new, changed, unchanged and gone compared to the previous mirror.
同時寫入 hts-changes.json列出這次擷取相對於上次鏡射的新增、變更、未變更與消失的項目。
Inline assets as data: URIs (self-contained pages)
將資源內嵌為 data: URI獨立網頁
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
鏡射完成後,重寫每個已儲存的網頁,將樣式表、指令碼、圖片與字型內嵌其中,讓網頁也能單獨開啟。
Largest inlined asset (bytes):
內嵌資源大小上限(位元組):
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
超過此大小的資源會保留一般連結;留空則使用預設的 10485760 位元組。
Seed the crawl from the site's sitemap
從網站的 Sitemap 開始擷取
Read the site's sitemap (robots.txt Sitemap: lines, then /sitemap.xml) and add every URL it lists as a start URL.
讀取網站的 Sitemaprobots.txt 中的 Sitemap: 行,然後 /sitemap.xml並將其中列出的每個網址加入為起始網址。
Sitemap address:
Sitemap 位址:
Address of a sitemap to read instead of probing the site; leave blank to probe robots.txt then /sitemap.xml.
要讀取的 Sitemap 位址,用來取代自動探測;留空則先探測 robots.txt 再探測 /sitemap.xml。

View File

@@ -964,3 +964,23 @@ WARC archive name:
WARC 归档名称:
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
WARC 归档的可选基本名称;留空则在输出目录中自动命名。
Report what changed since the previous mirror
报告自上次镜像以来的变更
Also write hts-changes.json listing what this crawl left new, changed, unchanged and gone compared to the previous mirror.
同时写入 hts-changes.json列出本次抓取相对于上次镜像的新增、更改、未更改和消失的项目。
Inline assets as data: URIs (self-contained pages)
将资源内嵌为 data: URI独立网页
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
镜像完成后,重写每个已保存的网页,将样式表、脚本、图片和字体内嵌其中,使网页也能单独打开。
Largest inlined asset (bytes):
内嵌资源大小上限(字节):
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
超过此大小的资源会保留普通链接;留空则使用默认的 10485760 字节。
Seed the crawl from the site's sitemap
从网站的 Sitemap 开始抓取
Read the site's sitemap (robots.txt Sitemap: lines, then /sitemap.xml) and add every URL it lists as a start URL.
读取网站的 Sitemaprobots.txt 中的 Sitemap: 行,然后 /sitemap.xml并将其中列出的每个网址添加为起始网址。
Sitemap address:
Sitemap 地址:
Address of a sitemap to read instead of probing the site; leave blank to probe robots.txt then /sitemap.xml.
要读取的 Sitemap 地址,用来代替自动探测;留空则先探测 robots.txt 再探测 /sitemap.xml。

View File

@@ -966,3 +966,23 @@ WARC archive name:
Naziv WARC arhive:
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
Neobavezni osnovni naziv WARC arhive; ostavite prazno za automatsko imenovanje u izlaznom direktoriju.
Report what changed since the previous mirror
Prijavi ¹to se promijenilo od prethodnog zrcaljenja
Also write hts-changes.json listing what this crawl left new, changed, unchanged and gone compared to the previous mirror.
Zapi¹i i hts-changes.json s popisom onoga ¹to je u odnosu na prethodno zrcaljenje novo, promijenjeno, nepromijenjeno ili nestalo.
Inline assets as data: URIs (self-contained pages)
Ugradi resurse kao data: URI-je (samostalne stranice)
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
Nakon dovr¹etka zrcaljenja ponovno zapi¹i svaku spremljenu stranicu s ugraðenim stilskim listovima, skriptama, slikama i fontovima, tako da se stranica mo¾e otvoriti i zasebno.
Largest inlined asset (bytes):
Najveæi ugraðeni resurs (bajtovi):
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
Resurs veæi od ove velièine zadr¾ava obiènu poveznicu; ostavite prazno za zadanih 10485760 bajtova.
Seed the crawl from the site's sitemap
Pokreni pretra¾ivanje iz karte web-mjesta
Read the site's sitemap (robots.txt Sitemap: lines, then /sitemap.xml) and add every URL it lists as a start URL.
Proèitaj kartu web-mjesta (retke Sitemap: iz robots.txt, zatim /sitemap.xml) i dodaj svaki navedeni URL kao poèetnu adresu.
Sitemap address:
Adresa karte web-mjesta:
Address of a sitemap to read instead of probing the site; leave blank to probe robots.txt then /sitemap.xml.
Adresa karte web-mjesta koju treba proèitati umjesto ispitivanja web-mjesta; ostavite prazno za ispitivanje robots.txt pa /sitemap.xml.

View File

@@ -1012,3 +1012,23 @@ WARC archive name:
Navn på WARC-arkiv:
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
Valgfrit basisnavn til WARC-arkivet; lad feltet stå tomt for automatisk navngivning i outputmappen.
Report what changed since the previous mirror
Rapportér hvad der er ændret siden den forrige spejling
Also write hts-changes.json listing what this crawl left new, changed, unchanged and gone compared to the previous mirror.
Skriv også hts-changes.json med en liste over, hvad denne gennemgang efterlader som nyt, ændret, uændret eller forsvundet i forhold til den forrige spejling.
Inline assets as data: URIs (self-contained pages)
Indlejr ressourcer som data:-URI'er (selvstændige sider)
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
Når spejlingen er færdig, omskrives hver gemt side med dens typografiark, scripts, billeder og skrifttyper indlejret, så en side også kan åbnes alene.
Largest inlined asset (bytes):
Største indlejrede ressource (byte):
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
En ressource over denne størrelse beholder et almindeligt link; lad feltet stå tomt for standardværdien på 10485760 byte.
Seed the crawl from the site's sitemap
Start gennemgangen fra webstedets sitemap
Read the site's sitemap (robots.txt Sitemap: lines, then /sitemap.xml) and add every URL it lists as a start URL.
Læs webstedets sitemap (Sitemap:-linjer i robots.txt, derefter /sitemap.xml) og tilføj hver angivet URL som startadresse.
Sitemap address:
Sitemap-adresse:
Address of a sitemap to read instead of probing the site; leave blank to probe robots.txt then /sitemap.xml.
Adressen på et sitemap, der skal læses i stedet for at undersøge webstedet; lad feltet stå tomt for at undersøge robots.txt og derefter /sitemap.xml.

View File

@@ -964,3 +964,23 @@ WARC archive name:
Name des WARC-Archivs:
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
Optionaler Basisname für das WARC-Archiv; leer lassen, um es automatisch im Ausgabeverzeichnis zu benennen.
Report what changed since the previous mirror
Melden, was sich seit der vorherigen Spiegelung geändert hat
Also write hts-changes.json listing what this crawl left new, changed, unchanged and gone compared to the previous mirror.
Zusätzlich hts-changes.json schreiben, das auflistet, was dieser Durchlauf gegenüber der vorherigen Spiegelung als neu, geändert, unverändert oder verschwunden hinterlässt.
Inline assets as data: URIs (self-contained pages)
Ressourcen als data:-URIs einbetten (eigenständige Seiten)
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
Nach Abschluss der Spiegelung jede gespeicherte Seite mit eingebetteten Stylesheets, Skripten, Bildern und Schriftarten neu schreiben, sodass eine Seite auch für sich allein geöffnet werden kann.
Largest inlined asset (bytes):
Größte eingebettete Ressource (Bytes):
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
Eine Ressource über dieser Größe behält einen gewöhnlichen Link; leer lassen für den Standardwert von 10485760 Bytes.
Seed the crawl from the site's sitemap
Erfassung mit der Sitemap der Website beginnen
Read the site's sitemap (robots.txt Sitemap: lines, then /sitemap.xml) and add every URL it lists as a start URL.
Die Sitemap der Website lesen (Sitemap:-Zeilen in robots.txt, dann /sitemap.xml) und jede dort aufgeführte URL als Startadresse hinzufügen.
Sitemap address:
Sitemap-Adresse:
Address of a sitemap to read instead of probing the site; leave blank to probe robots.txt then /sitemap.xml.
Adresse einer Sitemap, die anstelle der Suche auf der Website gelesen wird; leer lassen, um robots.txt und dann /sitemap.xml zu prüfen.

View File

@@ -964,3 +964,23 @@ WARC archive name:
WARC-arhiivi nimi:
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
WARC-arhiivi valikuline põhinimi; jäta tühjaks, et see väljundkataloogis automaatselt nimetada.
Report what changed since the previous mirror
Teata, mis on eelmisest peegeldusest saadik muutunud
Also write hts-changes.json listing what this crawl left new, changed, unchanged and gone compared to the previous mirror.
Kirjuta ka hts-changes.json, mis loetleb, mis on võrreldes eelmise peegeldusega uus, muutunud, muutumatu või kadunud.
Inline assets as data: URIs (self-contained pages)
Manusta ressursid data: URI-dena (iseseisvad lehed)
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
Pärast peegelduse lõppu kirjuta iga salvestatud leht ümber, manustades selle laaditabelid, skriptid, pildid ja fondid, nii et lehte saab avada ka eraldi.
Largest inlined asset (bytes):
Suurim manustatud ressurss (baiti):
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
Sellest suurem ressurss säilitab tavalise lingi; jäta tühjaks vaikeväärtuse 10485760 baiti jaoks.
Seed the crawl from the site's sitemap
Alusta kogumist saidi saidikaardist
Read the site's sitemap (robots.txt Sitemap: lines, then /sitemap.xml) and add every URL it lists as a start URL.
Loe saidi saidikaarti (robots.txt-i Sitemap:-read, seejärel /sitemap.xml) ja lisa iga seal loetletud URL alguslingina.
Sitemap address:
Saidikaardi aadress:
Address of a sitemap to read instead of probing the site; leave blank to probe robots.txt then /sitemap.xml.
Saidikaardi aadress, mida lugeda saidi sondeerimise asemel; jäta tühjaks, et kontrollida robots.txt-i ja seejärel /sitemap.xml-i.

View File

@@ -1012,3 +1012,23 @@ WARC archive name:
WARC archive name:
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
Report what changed since the previous mirror
Report what changed since the previous mirror
Also write hts-changes.json listing what this crawl left new, changed, unchanged and gone compared to the previous mirror.
Also write hts-changes.json listing what this crawl left new, changed, unchanged and gone compared to the previous mirror.
Inline assets as data: URIs (self-contained pages)
Inline assets as data: URIs (self-contained pages)
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
Largest inlined asset (bytes):
Largest inlined asset (bytes):
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
Seed the crawl from the site's sitemap
Seed the crawl from the site's sitemap
Read the site's sitemap (robots.txt Sitemap: lines, then /sitemap.xml) and add every URL it lists as a start URL.
Read the site's sitemap (robots.txt Sitemap: lines, then /sitemap.xml) and add every URL it lists as a start URL.
Sitemap address:
Sitemap address:
Address of a sitemap to read instead of probing the site; leave blank to probe robots.txt then /sitemap.xml.
Address of a sitemap to read instead of probing the site; leave blank to probe robots.txt then /sitemap.xml.

View File

@@ -966,3 +966,23 @@ WARC archive name:
WARC-arkiston nimi:
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
Valinnainen WARC-arkiston perusnimi; jätä tyhjäksi, jotta se nimetään automaattisesti tulostehakemistoon.
Report what changed since the previous mirror
Raportoi, mikä on muuttunut edellisen peilauksen jälkeen
Also write hts-changes.json listing what this crawl left new, changed, unchanged and gone compared to the previous mirror.
Kirjoita myös hts-changes.json, joka luettelee, mikä on edelliseen peilaukseen verrattuna uutta, muuttunutta, muuttumatonta tai kadonnutta.
Inline assets as data: URIs (self-contained pages)
Upota resurssit data:-URI-osoitteina (itsenäiset sivut)
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
Kun peilaus on valmis, kirjoita jokainen tallennettu sivu uudelleen niin, että sen tyylitiedostot, komentosarjat, kuvat ja fontit upotetaan, jolloin sivun voi avata myös yksinään.
Largest inlined asset (bytes):
Suurin upotettu resurssi (tavua):
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
Tätä suurempi resurssi säilyttää tavallisen linkin; jätä tyhjäksi, jolloin käytetään oletusarvoa 10485760 tavua.
Seed the crawl from the site's sitemap
Aloita haku sivuston sivukartasta
Read the site's sitemap (robots.txt Sitemap: lines, then /sitemap.xml) and add every URL it lists as a start URL.
Lue sivuston sivukartta (robots.txt-tiedoston Sitemap:-rivit, sitten /sitemap.xml) ja lisää jokainen siinä lueteltu URL-osoite aloitusosoitteeksi.
Sitemap address:
Sivukartan osoite:
Address of a sitemap to read instead of probing the site; leave blank to probe robots.txt then /sitemap.xml.
Luettavan sivukartan osoite sivuston luotaamisen sijaan; jätä tyhjäksi, jolloin tarkistetaan robots.txt ja sitten /sitemap.xml.

View File

@@ -1012,3 +1012,23 @@ WARC archive name:
Nom de l'archive WARC :
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
Nom de base optionnel pour l'archive WARC ; laissez vide pour le générer automatiquement dans le répertoire de sortie.
Report what changed since the previous mirror
Signaler ce qui a changé depuis le miroir précédent
Also write hts-changes.json listing what this crawl left new, changed, unchanged and gone compared to the previous mirror.
Écrire aussi hts-changes.json, qui liste ce que ce crawl laisse nouveau, modifié, inchangé ou disparu par rapport au miroir précédent.
Inline assets as data: URIs (self-contained pages)
Intégrer les ressources en URI data: (pages autonomes)
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
Une fois le miroir terminé, réécrire chaque page enregistrée avec ses feuilles de style, scripts, images et polices intégrés, afin qu'une page puisse aussi être ouverte seule.
Largest inlined asset (bytes):
Taille maximale d'une ressource intégrée (octets) :
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
Au-delà de cette taille, la ressource reste un lien ordinaire ; laissez vide pour la valeur par défaut de 10485760 octets.
Seed the crawl from the site's sitemap
Partir du plan de site (sitemap)
Read the site's sitemap (robots.txt Sitemap: lines, then /sitemap.xml) and add every URL it lists as a start URL.
Lire le plan de site (lignes Sitemap: de robots.txt, puis /sitemap.xml) et ajouter chaque URL listée comme adresse de départ.
Sitemap address:
Adresse du plan de site :
Address of a sitemap to read instead of probing the site; leave blank to probe robots.txt then /sitemap.xml.
Adresse d'un plan de site à lire au lieu de sonder le site ; laissez vide pour sonder robots.txt puis /sitemap.xml.

View File

@@ -966,3 +966,23 @@ WARC archive name:
Όνομα αρχείου WARC:
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
Προαιρετικό βασικό όνομα για το αρχείο WARC. Αφήστε το κενό για αυτόματη ονομασία στον κατάλογο εξόδου.
Report what changed since the previous mirror
Αναφορά των αλλαγών από το προηγούμενο αντίγραφο
Also write hts-changes.json listing what this crawl left new, changed, unchanged and gone compared to the previous mirror.
Εγγραφή και του hts-changes.json, που παραθέτει τι είναι νέο, τι άλλαξε, τι έμεινε ίδιο και τι χάθηκε σε σχέση με το προηγούμενο αντίγραφο.
Inline assets as data: URIs (self-contained pages)
Ενσωμάτωση των πόρων ως URI data: (αυτόνομες σελίδες)
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
Μόλις ολοκληρωθεί το αντίγραφο, κάθε αποθηκευμένη σελίδα ξαναγράφεται με ενσωματωμένα τα φύλλα στυλ, τα σενάρια, τις εικόνες και τις γραμματοσειρές της, ώστε η σελίδα να μπορεί να ανοίξει και μόνη της.
Largest inlined asset (bytes):
Μέγιστος ενσωματωμένος πόρος (byte):
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
Πόρος μεγαλύτερος από αυτό το μέγεθος διατηρεί κανονικό σύνδεσμο. Αφήστε το κενό για την προεπιλογή των 10485760 byte.
Seed the crawl from the site's sitemap
Έναρξη της ανίχνευσης από τον χάρτη του ιστότοπου
Read the site's sitemap (robots.txt Sitemap: lines, then /sitemap.xml) and add every URL it lists as a start URL.
Ανάγνωση του χάρτη του ιστότοπου (γραμμές Sitemap: στο robots.txt, έπειτα /sitemap.xml) και προσθήκη κάθε διεύθυνσης URL που περιέχει ως αρχικής διεύθυνσης.
Sitemap address:
Διεύθυνση χάρτη ιστότοπου:
Address of a sitemap to read instead of probing the site; leave blank to probe robots.txt then /sitemap.xml.
Διεύθυνση χάρτη ιστότοπου προς ανάγνωση αντί για αναζήτηση στον ιστότοπο. Αφήστε το κενό για έλεγχο του robots.txt και έπειτα του /sitemap.xml.

View File

@@ -964,3 +964,23 @@ WARC archive name:
Nome dell'archivio WARC:
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
Nome di base facoltativo per l'archivio WARC; lascia vuoto per assegnarlo automaticamente nella directory di output.
Report what changed since the previous mirror
Segnala che cosa è cambiato dalla copia precedente
Also write hts-changes.json listing what this crawl left new, changed, unchanged and gone compared to the previous mirror.
Scrive anche hts-changes.json, che elenca ciò che questa scansione lascia come nuovo, modificato, invariato o scomparso rispetto alla copia precedente.
Inline assets as data: URIs (self-contained pages)
Incorpora le risorse come URI data: (pagine autonome)
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
Al termine della copia, riscrive ogni pagina salvata incorporando fogli di stile, script, immagini e caratteri, in modo che una pagina possa essere aperta anche da sola.
Largest inlined asset (bytes):
Dimensione massima della risorsa incorporata (byte):
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
Una risorsa oltre questa dimensione mantiene un collegamento normale; lasciare vuoto per il valore predefinito di 10485760 byte.
Seed the crawl from the site's sitemap
Avvia la scansione dalla mappa del sito
Read the site's sitemap (robots.txt Sitemap: lines, then /sitemap.xml) and add every URL it lists as a start URL.
Legge la mappa del sito (righe Sitemap: in robots.txt, poi /sitemap.xml) e aggiunge come indirizzo iniziale ogni URL elencato.
Sitemap address:
Indirizzo della mappa del sito:
Address of a sitemap to read instead of probing the site; leave blank to probe robots.txt then /sitemap.xml.
Indirizzo di una mappa del sito da leggere invece di sondare il sito; lasciare vuoto per sondare robots.txt e poi /sitemap.xml.

View File

@@ -964,3 +964,23 @@ WARC archive name:
WARC アーカイブ名:
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
WARC アーカイブの任意のベース名。空欄にすると出力ディレクトリ内で自動的に名前が付けられます。
Report what changed since the previous mirror
前回のミラーからの変更点を報告する
Also write hts-changes.json listing what this crawl left new, changed, unchanged and gone compared to the previous mirror.
hts-changes.json も書き出し、前回のミラーと比べて新規、変更、変更なし、消滅となったものを一覧にします。
Inline assets as data: URIs (self-contained pages)
リソースを data: URI として埋め込む (単独で開けるページ)
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
ミラーの完了後、保存された各ページを、スタイルシート、スクリプト、画像、フォントを埋め込んだ形で書き直します。ページ単体でも開けるようになります。
Largest inlined asset (bytes):
埋め込む最大サイズ (バイト):
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
このサイズを超えるリソースは通常のリンクのままになります。空欄にすると既定値の 10485760 バイトになります。
Seed the crawl from the site's sitemap
サイトマップからミラーリングを開始する
Read the site's sitemap (robots.txt Sitemap: lines, then /sitemap.xml) and add every URL it lists as a start URL.
サイトマップ (robots.txt の Sitemap: 行、次に /sitemap.xml) を読み込み、記載されているすべての URL を開始アドレスとして追加します。
Sitemap address:
サイトマップのアドレス:
Address of a sitemap to read instead of probing the site; leave blank to probe robots.txt then /sitemap.xml.
サイトを探索する代わりに読み込むサイトマップのアドレス。空欄にすると robots.txt、次に /sitemap.xml を探索します。

View File

@@ -964,3 +964,23 @@ WARC archive name:
¸ÜÕ ÝÐ WARC ÐàåØÒÐâÐ:
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
¸×ÑÞàÝÞ ÞáÝÞÒÝÞ ØÜÕ ×Ð WARC ÐàåØÒÐâÐ; ÞáâÐÒÕâÕ ßàÐ×ÝÞ ×Ð ÐÒâÞÜÐâáÚÞ ØÜÕÝãÒÐúÕ ÒÞ Ø×ÛÕ×ÝØÞâ ÔØàÕÚâÞàØãÜ.
Report what changed since the previous mirror
¸×ÒÕáâØ èâÞ Õ ßàÞÜÕÝÕâÞ ÞÔ ßàÕâåÞÔÝÞâÞ ÞÓÛÕÔÐÛÞ
Also write hts-changes.json listing what this crawl left new, changed, unchanged and gone compared to the previous mirror.
·ÐßØèØ Ø hts-changes.json áÞ áߨáÞÚ ÝÐ âÞÐ èâÞ Õ ÝÞÒÞ, ßàÞÜÕÝÕâÞ, ÝÕßàÞÜÕÝÕâÞ ØÛØ ØáçÕ×ÝÐâÞ ÒÞ ÞÔÝÞá ÝÐ ßàÕâåÞÔÝÞâÞ ÞÓÛÕÔÐÛÞ.
Inline assets as data: URIs (self-contained pages)
²ÓàÐÔØ ÓØ àÕáãàáØâÕ ÚÐÚÞ data: URI (áÐÜÞáâÞøÝØ áâàÐÝØæØ)
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
¿Þ ×ÐÒàèãÒÐúÕ ÝÐ ÞÓÛÕÔÐÛÞâÞ, áÕÚÞøÐ ×ÐçãÒÐÝÐ áâàÐÝØæÐ áÕ ßàÕߨèãÒÐ áÞ ÒÓàÐÔÕÝØ áâØÛáÚØ ÛØáâÞÒØ, áÚàØßâØ, áÛØÚØ Ø äÞÝâÞÒØ, ×Ð ÔÐ ÜÞÖÕ áâàÐÝØæÐâÐ ÔÐ áÕ ÞâÒÞàØ Ø áÐÜÞáâÞøÝÞ.
Largest inlined asset (bytes):
½ÐøÓÞÛÕÜ ÒÓàÐÔÕÝ àÕáãàá (ÑÐøâØ):
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
ÀÕáãàá ßÞÓÞÛÕÜ ÞÔ ÞÒÐÐ ÓÞÛÕÜØÝÐ ×ÐÔàÖãÒÐ ÞÑØçÝÐ ÒàáÚÐ; ÞáâÐÒÕâÕ ßàÐ×ÝÞ ×Ð áâÐÝÔÐàÔÝØâÕ 10485760 ÑÐøâØ.
Seed the crawl from the site's sitemap
·ÐßÞçÝØ ÓÞ ßàÕÑÐàãÒÐúÕâÞ ÞÔ ÚÐàâÐâÐ ÝÐ áÐøâÞâ
Read the site's sitemap (robots.txt Sitemap: lines, then /sitemap.xml) and add every URL it lists as a start URL.
¿àÞçØâÐø øÐ ÚÐàâÐâÐ ÝÐ áÐøâÞâ (àÕÔÞÒØâÕ Sitemap: ÒÞ robots.txt, ßÞâÞÐ /sitemap.xml) Ø ÔÞÔÐø øÐ áÕÚÞøÐ ÝÐÒÕÔÕÝÐ URL ÐÔàÕáÐ ÚÐÚÞ ßÞçÕâÝÐ.
Sitemap address:
°ÔàÕáÐ ÝÐ ÚÐàâÐâÐ ÝÐ áÐøâÞâ:
Address of a sitemap to read instead of probing the site; leave blank to probe robots.txt then /sitemap.xml.
°ÔàÕáÐ ÝÐ ÚÐàâÐ ÝÐ áÐøâÞâ èâÞ âàÕÑÐ ÔÐ áÕ ßàÞçØâÐ ÝÐÜÕáâÞ ØáߨâãÒÐúÕ ÝÐ áÐøâÞâ; ÞáâÐÒÕâÕ ßàÐ×ÝÞ ×Ð ÔÐ áÕ ØáߨâÐ robots.txt, ßÐ /sitemap.xml.

View File

@@ -964,3 +964,23 @@ WARC archive name:
WARC archívum neve:
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
A WARC archívum opcionális alapneve; hagyja üresen az automatikus elnevezéshez a kimeneti könyvtárban.
Report what changed since the previous mirror
Jelentés arról, mi változott az elõzõ tükrözés óta
Also write hts-changes.json listing what this crawl left new, changed, unchanged and gone compared to the previous mirror.
A hts-changes.json fájl írása is, amely felsorolja, mi új, mi változott, mi maradt változatlan és mi tûnt el az elõzõ tükrözéshez képest.
Inline assets as data: URIs (self-contained pages)
Erõforrások beágyazása data: URI-ként (önálló oldalak)
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
A tükrözés befejezése után minden mentett oldal újraírása a stíluslapok, parancsfájlok, képek és betûtípusok beágyazásával, így az oldal önmagában is megnyitható.
Largest inlined asset (bytes):
Legnagyobb beágyazott erõforrás (bájt):
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
Az ennél nagyobb erõforrás közönséges hivatkozás marad; hagyja üresen a 10485760 bájtos alapértelmezéshez.
Seed the crawl from the site's sitemap
A letöltés indítása a webhely webhelytérképérõl
Read the site's sitemap (robots.txt Sitemap: lines, then /sitemap.xml) and add every URL it lists as a start URL.
A webhely webhelytérképének beolvasása (a robots.txt Sitemap: sorai, majd a /sitemap.xml), és a benne felsorolt összes URL felvétele kiindulási címként.
Sitemap address:
Webhelytérkép címe:
Address of a sitemap to read instead of probing the site; leave blank to probe robots.txt then /sitemap.xml.
A webhely vizsgálata helyett beolvasandó webhelytérkép címe; hagyja üresen a robots.txt, majd a /sitemap.xml vizsgálatához.

View File

@@ -964,3 +964,23 @@ WARC archive name:
Naam van WARC-archief:
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
Optionele basisnaam voor het WARC-archief; laat leeg om het automatisch een naam te geven in de uitvoermap.
Report what changed since the previous mirror
Rapporteren wat er sinds de vorige spiegeling is gewijzigd
Also write hts-changes.json listing what this crawl left new, changed, unchanged and gone compared to the previous mirror.
Ook hts-changes.json schrijven met een overzicht van wat deze doorloop nieuw, gewijzigd, ongewijzigd of verdwenen laat ten opzichte van de vorige spiegeling.
Inline assets as data: URIs (self-contained pages)
Bronnen insluiten als data:-URI's (op zichzelf staande pagina's)
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
Zodra de spiegeling klaar is, elke opgeslagen pagina herschrijven met ingesloten stijlbladen, scripts, afbeeldingen en lettertypen, zodat een pagina ook los geopend kan worden.
Largest inlined asset (bytes):
Grootste ingesloten bron (bytes):
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
Een bron boven deze grootte houdt een gewone koppeling; laat leeg voor de standaardwaarde van 10485760 bytes.
Seed the crawl from the site's sitemap
De crawl starten vanaf de sitemap van de site
Read the site's sitemap (robots.txt Sitemap: lines, then /sitemap.xml) and add every URL it lists as a start URL.
De sitemap van de site lezen (Sitemap:-regels in robots.txt, daarna /sitemap.xml) en elke vermelde URL als startadres toevoegen.
Sitemap address:
Sitemap-adres:
Address of a sitemap to read instead of probing the site; leave blank to probe robots.txt then /sitemap.xml.
Adres van een sitemap die gelezen moet worden in plaats van de site te onderzoeken; laat leeg om robots.txt en daarna /sitemap.xml te controleren.

View File

@@ -964,3 +964,23 @@ WARC archive name:
Navn på WARC-arkiv:
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
Valgfritt basisnavn for WARC-arkivet; la feltet stå tomt for automatisk navngivning i utdatamappen.
Report what changed since the previous mirror
Rapporter hva som er endret siden forrige speiling
Also write hts-changes.json listing what this crawl left new, changed, unchanged and gone compared to the previous mirror.
Skriv også hts-changes.json som lister opp hva denne gjennomgangen etterlater som nytt, endret, uendret eller forsvunnet sammenlignet med forrige speiling.
Inline assets as data: URIs (self-contained pages)
Bygg inn ressurser som data:-URI-er (selvstendige sider)
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
Når speilingen er ferdig, skrives hver lagrede side om med stilark, skript, bilder og skrifter innebygd, slik at en side også kan åpnes alene.
Largest inlined asset (bytes):
Største innebygde ressurs (byte):
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
En ressurs over denne størrelsen beholder en vanlig lenke; la feltet stå tomt for standardverdien på 10485760 byte.
Seed the crawl from the site's sitemap
Start gjennomgangen fra nettstedets nettstedskart
Read the site's sitemap (robots.txt Sitemap: lines, then /sitemap.xml) and add every URL it lists as a start URL.
Les nettstedets nettstedskart (Sitemap:-linjer i robots.txt, deretter /sitemap.xml) og legg til hver oppført URL som startadresse.
Sitemap address:
Adresse til nettstedskart:
Address of a sitemap to read instead of probing the site; leave blank to probe robots.txt then /sitemap.xml.
Adressen til et nettstedskart som skal leses i stedet for å undersøke nettstedet; la feltet stå tomt for å undersøke robots.txt og deretter /sitemap.xml.

View File

@@ -964,3 +964,23 @@ WARC archive name:
Nazwa archiwum WARC:
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
Opcjonalna nazwa bazowa archiwum WARC; pozostaw puste, aby nazwaæ je automatycznie w katalogu wyj¶ciowym.
Report what changed since the previous mirror
Zg³o¶, co zmieni³o siê od poprzedniej kopii
Also write hts-changes.json listing what this crawl left new, changed, unchanged and gone compared to the previous mirror.
Zapisz równie¿ plik hts-changes.json z list± tego, co w porównaniu z poprzedni± kopi± jest nowe, zmienione, niezmienione lub usuniête.
Inline assets as data: URIs (self-contained pages)
Osad¼ zasoby jako identyfikatory data: URI (samodzielne strony)
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
Po zakoñczeniu tworzenia kopii przepisz ka¿d± zapisan± stronê z osadzonymi arkuszami stylów, skryptami, obrazami i czcionkami, aby stronê mo¿na by³o otworzyæ równie¿ samodzielnie.
Largest inlined asset (bytes):
Najwiêkszy osadzony zasób (bajty):
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
Zasób wiêkszy ni¿ ten rozmiar zachowuje zwyk³y odno¶nik; pozostaw puste, aby u¿yæ domy¶lnych 10485760 bajtów.
Seed the crawl from the site's sitemap
Rozpocznij pobieranie od mapy witryny
Read the site's sitemap (robots.txt Sitemap: lines, then /sitemap.xml) and add every URL it lists as a start URL.
Odczytaj mapê witryny (wiersze Sitemap: w pliku robots.txt, nastêpnie /sitemap.xml) i dodaj ka¿dy wymieniony adres URL jako adres pocz±tkowy.
Sitemap address:
Adres mapy witryny:
Address of a sitemap to read instead of probing the site; leave blank to probe robots.txt then /sitemap.xml.
Adres mapy witryny do odczytania zamiast sondowania witryny; pozostaw puste, aby sprawdziæ robots.txt, a nastêpnie /sitemap.xml.

View File

@@ -1012,3 +1012,23 @@ WARC archive name:
Nome do arquivo WARC:
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
Nome base opcional para o arquivo WARC; deixe em branco para nomeá-lo automaticamente no diretório de saída.
Report what changed since the previous mirror
Relatar o que mudou desde o espelhamento anterior
Also write hts-changes.json listing what this crawl left new, changed, unchanged and gone compared to the previous mirror.
Gravar também hts-changes.json listando o que esta captura deixa como novo, alterado, inalterado ou removido em relação ao espelhamento anterior.
Inline assets as data: URIs (self-contained pages)
Incorporar os recursos como URIs data: (páginas autônomas)
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
Ao terminar o espelhamento, reescrever cada página salva com suas folhas de estilo, scripts, imagens e fontes incorporadas, para que a página também possa ser aberta sozinha.
Largest inlined asset (bytes):
Maior recurso incorporado (bytes):
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
Um recurso acima desse tamanho mantém um link comum; deixe em branco para o padrão de 10485760 bytes.
Seed the crawl from the site's sitemap
Iniciar a captura pelo mapa do site
Read the site's sitemap (robots.txt Sitemap: lines, then /sitemap.xml) and add every URL it lists as a start URL.
Ler o mapa do site (linhas Sitemap: do robots.txt, depois /sitemap.xml) e adicionar como endereço inicial cada URL nele listada.
Sitemap address:
Endereço do mapa do site:
Address of a sitemap to read instead of probing the site; leave blank to probe robots.txt then /sitemap.xml.
Endereço de um mapa do site a ser lido em vez de sondar o site; deixe em branco para sondar robots.txt e depois /sitemap.xml.

View File

@@ -964,3 +964,23 @@ WARC archive name:
Nome do arquivo WARC:
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
Nome base opcional para o arquivo WARC; deixe em branco para o nomear automaticamente no diretório de saída.
Report what changed since the previous mirror
Comunicar o que mudou desde o espelho anterior
Also write hts-changes.json listing what this crawl left new, changed, unchanged and gone compared to the previous mirror.
Escrever também hts-changes.json, que lista o que esta recolha deixa como novo, alterado, inalterado ou desaparecido em relação ao espelho anterior.
Inline assets as data: URIs (self-contained pages)
Incorporar os recursos como URI data: (páginas autónomas)
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
Depois de terminado o espelho, reescrever cada página guardada com as suas folhas de estilo, scripts, imagens e tipos de letra incorporados, para que a página também possa ser aberta sozinha.
Largest inlined asset (bytes):
Maior recurso incorporado (bytes):
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
Um recurso acima deste tamanho mantém uma ligação normal; deixe em branco para o valor predefinido de 10485760 bytes.
Seed the crawl from the site's sitemap
Iniciar a recolha pelo mapa do site
Read the site's sitemap (robots.txt Sitemap: lines, then /sitemap.xml) and add every URL it lists as a start URL.
Ler o mapa do site (linhas Sitemap: do robots.txt, depois /sitemap.xml) e adicionar como endereço inicial cada URL nele listado.
Sitemap address:
Endereço do mapa do site:
Address of a sitemap to read instead of probing the site; leave blank to probe robots.txt then /sitemap.xml.
Endereço de um mapa do site a ler em vez de sondar o site; deixe em branco para sondar robots.txt e depois /sitemap.xml.

View File

@@ -964,3 +964,23 @@ WARC archive name:
Numele arhivei WARC:
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
Nume de baza optional pentru arhiva WARC; lasati gol pentru a-l denumi automat in directorul de iesire.
Report what changed since the previous mirror
Raporteaza ce s-a schimbat fata de copia anterioara
Also write hts-changes.json listing what this crawl left new, changed, unchanged and gone compared to the previous mirror.
Scrie si hts-changes.json, care listeaza ce este nou, modificat, nemodificat sau disparut fata de copia anterioara.
Inline assets as data: URIs (self-contained pages)
Încorporeaza resursele ca URI data: (pagini de sine statatoare)
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
Dupa terminarea copiei, fiecare pagina salvata este rescrisa cu foile de stil, scripturile, imaginile si fonturile încorporate, astfel încât pagina sa poata fi deschisa si singura.
Largest inlined asset (bytes):
Cea mai mare resursa încorporata (octeti):
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
O resursa mai mare decât aceasta dimensiune pastreaza o legatura obisnuita; lasati gol pentru valoarea implicita de 10485760 de octeti.
Seed the crawl from the site's sitemap
Porneste explorarea de la harta sitului
Read the site's sitemap (robots.txt Sitemap: lines, then /sitemap.xml) and add every URL it lists as a start URL.
Citeste harta sitului (liniile Sitemap: din robots.txt, apoi /sitemap.xml) si adauga fiecare URL listat ca adresa de pornire.
Sitemap address:
Adresa hartii sitului:
Address of a sitemap to read instead of probing the site; leave blank to probe robots.txt then /sitemap.xml.
Adresa unei harti a sitului care sa fie citita în loc de sondarea sitului; lasati gol pentru a sonda robots.txt, apoi /sitemap.xml.

View File

@@ -964,3 +964,23 @@ WARC archive name:
Èìÿ WARC-àðõèâà:
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
Íåîáÿçàòåëüíîå áàçîâîå èìÿ WARC-àðõèâà; îñòàâüòå ïóñòûì äëÿ àâòîìàòè÷åñêîãî èìåíîâàíèÿ â âûõîäíîì êàòàëîãå.
Report what changed since the previous mirror
Ñîîáùàòü, ÷òî èçìåíèëîñü ñ ïðåäûäóùåãî çåðêàëà
Also write hts-changes.json listing what this crawl left new, changed, unchanged and gone compared to the previous mirror.
Òàêæå çàïèñûâàòü hts-changes.json ñî ñïèñêîì òîãî, ÷òî ïî ñðàâíåíèþ ñ ïðåäûäóùèì çåðêàëîì ñòàëî íîâûì, èçìåí¸ííûì, íåèçìåí¸ííûì èëè èñ÷åçëî.
Inline assets as data: URIs (self-contained pages)
Âñòðàèâàòü ðåñóðñû êàê data: URI (ñàìîñòîÿòåëüíûå ñòðàíèöû)
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
Ïîñëå çàâåðøåíèÿ çåðêàëèðîâàíèÿ êàæäàÿ ñîõðàí¸ííàÿ ñòðàíèöà ïåðåçàïèñûâàåòñÿ ñî âñòðîåííûìè òàáëèöàìè ñòèëåé, ñöåíàðèÿìè, èçîáðàæåíèÿìè è øðèôòàìè, ÷òîáû ñòðàíèöó ìîæíî áûëî îòêðûòü îòäåëüíî.
Largest inlined asset (bytes):
Íàèáîëüøèé âñòðàèâàåìûé ðåñóðñ (áàéòû):
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
Ðåñóðñ áîëüøå ýòîãî ðàçìåðà ñîõðàíÿåò îáû÷íóþ ññûëêó; îñòàâüòå ïóñòûì äëÿ çíà÷åíèÿ ïî óìîë÷àíèþ 10485760 áàéò.
Seed the crawl from the site's sitemap
Íà÷èíàòü îáõîä ñ êàðòû ñàéòà
Read the site's sitemap (robots.txt Sitemap: lines, then /sitemap.xml) and add every URL it lists as a start URL.
Ïðî÷èòàòü êàðòó ñàéòà (ñòðîêè Sitemap: â robots.txt, çàòåì /sitemap.xml) è äîáàâèòü êàæäûé óêàçàííûé â íåé URL êàê íà÷àëüíûé àäðåñ.
Sitemap address:
Àäðåñ êàðòû ñàéòà:
Address of a sitemap to read instead of probing the site; leave blank to probe robots.txt then /sitemap.xml.
Àäðåñ êàðòû ñàéòà, êîòîðóþ íóæíî ïðî÷èòàòü âìåñòî îïðîñà ñàéòà; îñòàâüòå ïóñòûì, ÷òîáû ïðîâåðèòü robots.txt, çàòåì /sitemap.xml.

View File

@@ -964,3 +964,23 @@ WARC archive name:
Názov archívu WARC:
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
Voliteµný základný názov archívu WARC; ponechajte prázdne pre automatické pomenovanie vo výstupnom adresári.
Report what changed since the previous mirror
Oznámi», èo sa od predchádzajúceho zrkadlenia zmenilo
Also write hts-changes.json listing what this crawl left new, changed, unchanged and gone compared to the previous mirror.
Zapísa» aj hts-changes.json so zoznamom toho, èo je oproti predchádzajúcemu zrkadleniu nové, zmenené, nezmenené alebo chýbajúce.
Inline assets as data: URIs (self-contained pages)
Vlo¾i» zdroje ako URI data: (samostatné stránky)
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
Po dokonèení zrkadlenia prepísa» ka¾dú ulo¾enú stránku s vlo¾enými ¹týlmi, skriptmi, obrázkami a písmami, aby sa stránka dala otvori» aj samostatne.
Largest inlined asset (bytes):
Najväè¹í vlo¾ený zdroj (bajty):
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
Zdroj väè¹í ne¾ táto veµkos» si ponechá be¾ný odkaz; ponechajte prázdne pre predvolených 10485760 bajtov.
Seed the crawl from the site's sitemap
Zaèa» prehliadanie z mapy stránok
Read the site's sitemap (robots.txt Sitemap: lines, then /sitemap.xml) and add every URL it lists as a start URL.
Naèíta» mapu stránok (riadky Sitemap: v súbore robots.txt, potom /sitemap.xml) a prida» ka¾dú uvedenú adresu URL ako poèiatoènú.
Sitemap address:
Adresa mapy stránok:
Address of a sitemap to read instead of probing the site; leave blank to probe robots.txt then /sitemap.xml.
Adresa mapy stránok, ktorá sa má naèíta» namiesto zis»ovania na stránke; ponechajte prázdne na zistenie z robots.txt a potom /sitemap.xml.

View File

@@ -964,3 +964,23 @@ WARC archive name:
Ime arhiva WARC:
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
Neobvezno osnovno ime arhiva WARC; pustite prazno za samodejno poimenovanje v izhodni mapi.
Report what changed since the previous mirror
Porocaj, kaj se je spremenilo od prejsnjega zrcaljenja
Also write hts-changes.json listing what this crawl left new, changed, unchanged and gone compared to the previous mirror.
Zapisi tudi hts-changes.json s seznamom tega, kar je v primerjavi s prejsnjim zrcaljenjem novo, spremenjeno, nespremenjeno ali izginilo.
Inline assets as data: URIs (self-contained pages)
Vgradi vire kot data: URI (samostojne strani)
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
Ko je zrcaljenje koncano, se vsaka shranjena stran prepise z vgrajenimi slogovnimi listi, skripti, slikami in pisavami, tako da jo je mogoce odpreti tudi samostojno.
Largest inlined asset (bytes):
Najvecji vgrajeni vir (bajti):
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
Vir, vecji od te velikosti, ohrani obicajno povezavo; pustite prazno za privzetih 10485760 bajtov.
Seed the crawl from the site's sitemap
Zacni zajem z zemljevidom spletnega mesta
Read the site's sitemap (robots.txt Sitemap: lines, then /sitemap.xml) and add every URL it lists as a start URL.
Preberi zemljevid spletnega mesta (vrstice Sitemap: v robots.txt, nato /sitemap.xml) in dodaj vsak navedeni URL kot zacetni naslov.
Sitemap address:
Naslov zemljevida spletnega mesta:
Address of a sitemap to read instead of probing the site; leave blank to probe robots.txt then /sitemap.xml.
Naslov zemljevida spletnega mesta, ki naj se prebere namesto preverjanja mesta; pustite prazno za preverjanje robots.txt in nato /sitemap.xml.

View File

@@ -964,3 +964,23 @@ WARC archive name:
WARC-arkivets namn:
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
Valfritt basnamn för WARC-arkivet; lämna tomt för att namnge det automatiskt i utdatakatalogen.
Report what changed since the previous mirror
Rapportera vad som ändrats sedan föregående spegling
Also write hts-changes.json listing what this crawl left new, changed, unchanged and gone compared to the previous mirror.
Skriv även hts-changes.json som listar vad den här genomgången lämnar som nytt, ändrat, oförändrat eller försvunnet jämfört med föregående spegling.
Inline assets as data: URIs (self-contained pages)
Bädda in resurser som data:-URI:er (fristående sidor)
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
När speglingen är klar skrivs varje sparad sida om med sina formatmallar, skript, bilder och teckensnitt inbäddade, så att en sida också kan öppnas för sig.
Largest inlined asset (bytes):
Största inbäddade resurs (byte):
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
En resurs över den här storleken behåller en vanlig länk; lämna tomt för standardvärdet 10485760 byte.
Seed the crawl from the site's sitemap
Starta insamlingen från webbplatsens webbplatskarta
Read the site's sitemap (robots.txt Sitemap: lines, then /sitemap.xml) and add every URL it lists as a start URL.
Läs webbplatsens webbplatskarta (Sitemap:-rader i robots.txt, sedan /sitemap.xml) och lägg till varje angiven URL som startadress.
Sitemap address:
Webbplatskartans adress:
Address of a sitemap to read instead of probing the site; leave blank to probe robots.txt then /sitemap.xml.
Adress till en webbplatskarta som ska läsas i stället för att söka på webbplatsen; lämna tomt för att kontrollera robots.txt och sedan /sitemap.xml.

View File

@@ -964,3 +964,23 @@ WARC archive name:
WARC arþivi adý:
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
WARC arþivi için isteðe baðlý temel ad; çýktý dizininde otomatik adlandýrma için boþ býrakýn.
Report what changed since the previous mirror
Önceki yansýmadan bu yana deðiþenleri bildir
Also write hts-changes.json listing what this crawl left new, changed, unchanged and gone compared to the previous mirror.
Ayrýca hts-changes.json yazarak bu taramanýn önceki yansýmaya göre neyi yeni, deðiþmiþ, deðiþmemiþ veya kaybolmuþ býraktýðýný listele.
Inline assets as data: URIs (self-contained pages)
Kaynaklarý data: URI olarak göm (kendi kendine yeten sayfalar)
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
Yansýlama bittiðinde, kaydedilen her sayfa stil sayfalarý, betikleri, görselleri ve yazý tipleri gömülü olarak yeniden yazýlýr; böylece sayfa tek baþýna da açýlabilir.
Largest inlined asset (bytes):
En büyük gömülü kaynak (bayt):
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
Bu boyutun üzerindeki bir kaynak sýradan baðlantýsýný korur; 10485760 baytlýk varsayýlan için boþ býrakýn.
Seed the crawl from the site's sitemap
Taramayý sitenin site haritasýndan baþlat
Read the site's sitemap (robots.txt Sitemap: lines, then /sitemap.xml) and add every URL it lists as a start URL.
Sitenin site haritasýný oku (robots.txt içindeki Sitemap: satýrlarý, ardýndan /sitemap.xml) ve listelenen her URL'yi baþlangýç adresi olarak ekle.
Sitemap address:
Site haritasý adresi:
Address of a sitemap to read instead of probing the site; leave blank to probe robots.txt then /sitemap.xml.
Siteyi yoklamak yerine okunacak site haritasýnýn adresi; robots.txt ve ardýndan /sitemap.xml yoklamasý için boþ býrakýn.

View File

@@ -964,3 +964,23 @@ WARC archive name:
²ì'ÿ WARC-àðõ³âó:
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
Íåîáîâ'ÿçêîâà áàçîâà íàçâà WARC-àðõ³âó; çàëèøòå ïîðîæí³ì äëÿ àâòîìàòè÷íîãî íàéìåíóâàííÿ ó âèõ³äíîìó êàòàëîç³.
Report what changed since the previous mirror
Ïîâ³äîìëÿòè, ùî çì³íèëîñÿ ç ïîïåðåäíüîãî äçåðêàëà
Also write hts-changes.json listing what this crawl left new, changed, unchanged and gone compared to the previous mirror.
Òàêîæ çàïèñóâàòè hts-changes.json ç³ ñïèñêîì òîãî, ùî ïîð³âíÿíî ç ïîïåðåäí³ì äçåðêàëîì º íîâèì, çì³íåíèì, íåçì³íåíèì àáî çíèêëèì.
Inline assets as data: URIs (self-contained pages)
Âáóäîâóâàòè ðåñóðñè ÿê data: URI (ñàìîñò³éí³ ñòîð³íêè)
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
ϳñëÿ çàâåðøåííÿ äçåðêàëþâàííÿ êîæíà çáåðåæåíà ñòîð³íêà ïåðåçàïèñóºòüñÿ ç âáóäîâàíèìè òàáëèöÿìè ñòèë³â, ñöåíàð³ÿìè, çîáðàæåííÿìè òà øðèôòàìè, ùîá ñòîð³íêó ìîæíà áóëî â³äêðèòè îêðåìî.
Largest inlined asset (bytes):
Íàéá³ëüøèé âáóäîâàíèé ðåñóðñ (áàéòè):
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
Ðåñóðñ, á³ëüøèé çà öåé ðîçì³ð, çáåð³ãຠçâè÷àéíå ïîñèëàííÿ; çàëèøòå ïîðîæí³ì äëÿ òèïîâîãî çíà÷åííÿ 10485760 áàéò³â.
Seed the crawl from the site's sitemap
Ïî÷èíàòè îáõ³ä ç êàðòè ñàéòó
Read the site's sitemap (robots.txt Sitemap: lines, then /sitemap.xml) and add every URL it lists as a start URL.
Ïðî÷èòàòè êàðòó ñàéòó (ðÿäêè Sitemap: ó robots.txt, ïîò³ì /sitemap.xml) ³ äîäàòè êîæíó âêàçàíó â í³é URL-àäðåñó ÿê ïî÷àòêîâó.
Sitemap address:
Àäðåñà êàðòè ñàéòó:
Address of a sitemap to read instead of probing the site; leave blank to probe robots.txt then /sitemap.xml.
Àäðåñà êàðòè ñàéòó, ÿêó ñë³ä ïðî÷èòàòè çàì³ñòü îïèòóâàííÿ ñàéòó; çàëèøòå ïîðîæí³ì, ùîá ïåðåâ³ðèòè robots.txt, à ïîò³ì /sitemap.xml.

View File

@@ -964,3 +964,23 @@ WARC archive name:
WARC arxivi nomi:
Optional base name for the WARC archive; leave blank to auto-name it under the output directory.
WARC arxivi uchun ixtiyoriy asosiy nom; chiqish katalogida avtomatik nomlash uchun bo'sh qoldiring.
Report what changed since the previous mirror
Oldingi nusxadan beri nima ozgarganini xabar qilish
Also write hts-changes.json listing what this crawl left new, changed, unchanged and gone compared to the previous mirror.
hts-changes.json ham yoziladi: unda ushbu yigish oldingi nusxaga nisbatan nimani yangi, ozgargan, ozgarmagan yoki yoqolgan holda qoldirgani royxati boladi.
Inline assets as data: URIs (self-contained pages)
Resurslarni data: URI sifatida joylash (mustaqil sahifalar)
Once the mirror is finished, rewrite every saved page with its stylesheets, scripts, images and fonts embedded, so a page can also be opened on its own.
Nusxalash tugagach, har bir saqlangan sahifa uslublar jadvallari, skriptlar, rasmlar va shriftlar joylangan holda qayta yoziladi, shunda sahifani alohida ham ochish mumkin.
Largest inlined asset (bytes):
Eng katta joylangan resurs (bayt):
An asset above this size keeps an ordinary link; leave blank for the 10485760-byte default.
Bu olchamdan katta resurs oddiy havolani saqlab qoladi; standart 10485760 bayt uchun bosh qoldiring.
Seed the crawl from the site's sitemap
Yigishni saytning sayt xaritasidan boshlash
Read the site's sitemap (robots.txt Sitemap: lines, then /sitemap.xml) and add every URL it lists as a start URL.
Saytning sayt xaritasini oqish (robots.txt dagi Sitemap: qatorlari, songra /sitemap.xml) va unda korsatilgan har bir URL manzilni boshlangich manzil sifatida qoshish.
Sitemap address:
Sayt xaritasi manzili:
Address of a sitemap to read instead of probing the site; leave blank to probe robots.txt then /sitemap.xml.
Saytni tekshirish orniga oqiladigan sayt xaritasi manzili; robots.txt, songra /sitemap.xml ni tekshirish uchun bosh qoldiring.

View File

@@ -71,7 +71,9 @@ static int mysavename(t_hts_callbackarg * carg, httrackp * opt,
for(j = 0; iisBogus[i][j] == a[j] && iisBogus[i][j] != '\0'; j++) ;
if (iisBogus[i][j] == '\0'
&& (a[j] == '\0' || a[j] == '/' || a[j] == '\\')) {
strncpy(a, iisBogusReplace[i], strlen(iisBogusReplace[i]));
/* j bytes matched, so j fit: copying j cannot overrun whatever the
table holds, and the tail must survive untouched */
memcpy(a, iisBogusReplace[i], (size_t) j);
break;
}
}

View File

@@ -1,82 +1,33 @@
dnl @synopsis CHECK_ZLIB()
dnl
dnl This macro searches for an installed zlib library. If nothing
dnl was specified when calling configure, it searches first in /usr/local
dnl and then in /usr. If the --with-zlib=DIR is specified, it will try
dnl to find it in DIR/include/zlib.h and DIR/lib/libz.a. If --without-zlib
dnl is specified, the library is not searched at all.
dnl
dnl If either the header file (zlib.h) or the library (libz) is not
dnl found, the configuration exits on error, asking for a valid
dnl zlib installation directory or --without-zlib.
dnl
dnl The macro defines the symbol HAVE_LIBZ if the library is found. You should
dnl use autoheader to include a definition for this symbol in a config.h
dnl file. Sample usage in a C/C++ source is as follows:
dnl
dnl #ifdef HAVE_LIBZ
dnl #include <zlib.h>
dnl #endif /* HAVE_LIBZ */
dnl
dnl @version $Id$
dnl @author Loic Dachary <loic@senga.org>
dnl Look for zlib. It is a hard requirement, not an option: the cache and the
dnl WARC output are zip/gzip containers, and the bundled minizip calls zlib
dnl directly. --with-zlib=DIR points at a non-standard prefix.
dnl
dnl Adds -lz to LIBS and defines HAVE_LIBZ.
AC_DEFUN([CHECK_ZLIB],
#
# Handle user hints
#
[AC_MSG_CHECKING(if zlib is wanted)
AC_ARG_WITH(zlib,
[ --with-zlib=DIR root directory path of zlib installation [defaults to
/usr/local or /usr if not found in /usr/local]
--without-zlib to disable zlib usage completely],
[if test "$withval" != no ; then
AC_MSG_RESULT(yes)
ZLIB_HOME="$withval"
else
AC_MSG_RESULT(no)
fi], [
AC_MSG_RESULT(yes)
ZLIB_HOME=/usr/local
if test ! -f "${ZLIB_HOME}/include/zlib.h"
then
ZLIB_HOME=/usr
AC_DEFUN([CHECK_ZLIB], [
AC_ARG_WITH([zlib],
[AS_HELP_STRING([--with-zlib=DIR],[root directory of the zlib installation])],
[zlib_want=$withval], [zlib_want=yes])
if test "$zlib_want" = "no"; then
AC_MSG_ERROR([zlib cannot be disabled: the cache and the WARC output are zip/gzip containers, and the bundled minizip calls zlib directly])
fi
])
#
# Locate zlib, if wanted
#
if test -n "${ZLIB_HOME}"
then
ZLIB_OLD_LDFLAGS=$LDFLAGS
ZLIB_OLD_CPPFLAGS=$LDFLAGS
LDFLAGS="$LDFLAGS -L${ZLIB_HOME}/lib"
CPPFLAGS="$CPPFLAGS -I${ZLIB_HOME}/include"
AC_LANG_SAVE
AC_LANG_C
AC_CHECK_LIB(z, inflateEnd, [zlib_cv_libz=yes], [zlib_cv_libz=no])
AC_CHECK_HEADER(zlib.h, [zlib_cv_zlib_h=yes], [zlib_cv_zlib_h=no])
AC_LANG_RESTORE
if test "$zlib_cv_libz" = "yes" -a "$zlib_cv_zlib_h" = "yes"
then
#
# If both library and header were found, use them
#
AC_CHECK_LIB(z, inflateEnd)
AC_MSG_CHECKING(zlib in ${ZLIB_HOME})
AC_MSG_RESULT(ok)
else
#
# If either header or library was not found, revert and bomb
#
AC_MSG_CHECKING(zlib in ${ZLIB_HOME})
LDFLAGS="$ZLIB_OLD_LDFLAGS"
CPPFLAGS="$ZLIB_OLD_CPPFLAGS"
AC_MSG_RESULT(failed)
AC_MSG_ERROR(either specify a valid zlib installation with --with-zlib=DIR or disable zlib usage with --without-zlib)
fi
if test "$zlib_want" != "yes"; then
# An explicit prefix is authoritative: if the header is not under it,
# error rather than silently pick a system copy.
if test ! -f "$zlib_want/include/zlib.h"; then
AC_MSG_ERROR([zlib requested at $zlib_want but $zlib_want/include/zlib.h is missing])
fi
CPPFLAGS="$CPPFLAGS -I$zlib_want/include"
LDFLAGS="$LDFLAGS -L$zlib_want/lib"
elif test -f /usr/local/include/zlib.h; then
# Where the BSD ports tree lands zlib, and not always searched by default.
CPPFLAGS="$CPPFLAGS -I/usr/local/include"
LDFLAGS="$LDFLAGS -L/usr/local/lib"
fi
AC_CHECK_HEADER([zlib.h], [],
[AC_MSG_ERROR([zlib.h not found; install the zlib development files or pass --with-zlib=DIR])])
AC_CHECK_LIB([z], [inflateEnd], [],
[AC_MSG_ERROR([libz not found; install the zlib development files or pass --with-zlib=DIR])])
])

View File

@@ -3,7 +3,7 @@
.\"
.\" This file is generated by man/makeman.sh; do not edit by hand.
.\" SPDX-License-Identifier: GPL-3.0-or-later
.TH httrack 1 "23 July 2026" "httrack website copier"
.TH httrack 1 "27 July 2026" "httrack website copier"
.SH NAME
httrack \- offline browser : copy websites to a local directory
.SH SYNOPSIS
@@ -36,10 +36,12 @@ httrack \- offline browser : copy websites to a local directory
[ \fB\-t, \-\-test\fR ]
[ \fB\-%L, \-\-list\fR ]
[ \fB\-%S, \-\-urllist\fR ]
[ \fB\-%m, \-\-sitemap\fR ]
[ \fB\-NN, \-\-structure[=N]\fR ]
[ \fB\-%N, \-\-delayed\-type\-check\fR ]
[ \fB\-%D, \-\-cached\-delayed\-type\-check\fR ]
[ \fB\-%M, \-\-mime\-html\fR ]
[ \fB\-%Z, \-\-single\-file\fR ]
[ \fB\-LN, \-\-long\-names[=N]\fR ]
[ \fB\-KN, \-\-keep\-links[=N]\fR ]
[ \fB\-x, \-\-replace\-external\fR ]
@@ -75,6 +77,7 @@ httrack \- offline browser : copy websites to a local directory
[ \fB\-C, \-\-cache[=N]\fR ]
[ \fB\-k, \-\-store\-all\-in\-cache\fR ]
[ \fB\-%r, \-\-warc\fR ]
[ \fB\-%d, \-\-changes\fR ]
[ \fB\-%n, \-\-do\-not\-recatch\fR ]
[ \fB\-%v, \-\-display\fR ]
[ \fB\-Q, \-\-do\-not\-log\fR ]
@@ -187,6 +190,8 @@ test all URLs (even forbidden ones) (\-\-test)
<file> add all URL located in this text file (one URL per line) (\-\-list <param>)
.IP \-%S
<file> add all scan rules located in this text file (one scan rule per line) (\-\-urllist <param>)
.IP \-%m
seed the crawl from the site's sitemap (robots.txt Sitemap:, then /sitemap.xml); \-\-sitemap\-url URL names one explicitly. A sitemap you name, or one the site declares, is fetched even under robots.txt Disallow; only the guessed /sitemap.xml obeys it. The URLs found still pass every filter and scope rule (\-\-sitemap)
.SS Build options:
.IP \-NN
structure type (0 *original structure, 1+: see below) (\-\-structure[=N])
@@ -198,6 +203,8 @@ delayed type check, don't make any link test but wait for files download to star
cached delayed type check, don't wait for remote type during updates, to speedup them (%D0 wait, * %D1 don't wait) (\-\-cached\-delayed\-type\-check)
.IP \-%M
generate a RFC MIME\-encapsulated full\-archive (.mht) (\-\-mime\-html)
.IP \-%Z
after the mirror, rewrite each saved page with its stylesheets, scripts, images and fonts inlined as data: URIs, so any page opens by double\-click anywhere (links between pages stay relative; audio and video stay links); \-\-single\-file\-max\-size N caps each asset (default 10485760 bytes). %M is the better container where a Chromium\-family browser is a given: one archive, no base64 tax on text, a shared asset stored once (\-\-single\-file)
.IP \-%t
keep the original file extension, don't rewrite it from the MIME type (%t0 rewrite)
.IP \-LN
@@ -213,7 +220,7 @@ do not include any password for external password protected websites (%x0 includ
.IP \-%g
strip query keys for dedup ([host/pattern=]key1,key2,...) (\-\-strip\-query <param>)
.IP \-o
*generate output html file in case of error (404..) (o0 don't generate) (\-\-generate\-errors)
*save the server's error pages (404..) (o0 discard them) (\-\-generate\-errors)
.IP \-X
*purge old files after update (X0 keep delete) (\-\-purge\-old[=N])
.IP \-%p
@@ -279,6 +286,8 @@ create/use a cache for updates and retries (C0 no cache,C1 cache is prioritary,*
store all files in cache (not useful if files on disk) (\-\-store\-all\-in\-cache)
.IP \-%r
write an ISO\-28500 WARC/1.1 archive; \-\-warc\-file NAME sets the output name, \-\-warc\-max\-size N rotates segments past N bytes, \-\-warc\-cdx also writes a sorted CDXJ index, \-\-wacz packages it all as a WACZ file (\-\-warc)
.IP \-%d
write hts\-changes.json listing what this crawl left new, changed, unchanged and gone compared to the previous mirror (\-\-changes)
.IP \-%n
do not re\-download locally erased files (\-\-do\-not\-recatch)
.IP \-%v

View File

@@ -32,7 +32,9 @@ AM_LDFLAGS = \
bin_PROGRAMS = proxytrack httrack htsserver
httrack_LDADD = $(THREADS_LIBS) libhttrack.la
httrack_SOURCES = httrack.c htsbacktrace.c htsbacktrace.h
# $(DL_LIBS): dladdr() in the crash handler, still in libdl on pre-2.34 glibc.
httrack_LDADD = $(THREADS_LIBS) $(DL_LIBS) libhttrack.la
htsserver_LDADD = $(THREADS_LIBS) $(SOCKET_LIBS) libhttrack.la
proxytrack_LDADD = $(THREADS_LIBS) $(SOCKET_LIBS)
@@ -46,7 +48,8 @@ htsserver_LDFLAGS = $(AM_LDFLAGS) $(LDFLAGS_PIE)
lib_LTLIBRARIES = libhttrack.la
htsserver_SOURCES = htsserver.c htsserver.h htsweb.c htsweb.h \
htsserver_SOURCES = htsserver.c htsserver.h htsweb.c htsweb.h htsstats.h \
htscmdline.c htscmdline.h \
htsurlport.c htsurlport.h
proxytrack_SOURCES = proxy/main.c \
proxy/proxytrack.c proxy/store.c \
@@ -60,21 +63,21 @@ whttrackrun_SCRIPTS = webhttrack
libhttrack_la_SOURCES = htscore.c htsparse.c htsback.c htscache.c \
htscache_selftest.c htsdns_selftest.c htsselftest.c \
htscatchurl.c htsfilters.c htsftp.c htshash.c coucal/coucal.c \
htshelp.c htslib.c htsurlport.c htscoremain.c \
htscmdline.c htshelp.c htslib.c htsurlport.c htscoremain.c \
htsname.c htsrobots.c htstools.c htswizard.c \
htsalias.c htsthread.c htsindex.c htsbauth.c \
htsmd5.c htscodec.c htswarc.c htsproxy.c htszlib.c htswrap.c htsconcat.c \
htsmd5.c htscodec.c htswarc.c htschanges.c htssinglefile.c htssitemap.c htsproxy.c htszlib.c htswrap.c htsconcat.c \
htsmodules.c htscharset.c punycode.c htsencoding.c htssniff.c \
md5.c \
minizip/ioapi.c minizip/mztools.c minizip/unzip.c minizip/zip.c \
hts-indextmpl.h htsalias.h htsback.h htsbase.h htssafe.h \
htsbasenet.h htsbauth.h htscache.h htscache_selftest.h htsdns_selftest.h htsselftest.h htscatchurl.h \
htsconfig.h htscore.h htsparse.h htscoremain.h htsdefines.h \
htscmdline.h htsconfig.h htscore.h htsparse.h htscoremain.h htsdefines.h \
htsfilters.h htsftp.h htsglobal.h htshash.h coucal/coucal.h \
htshelp.h htsindex.h htslib.h htsurlport.h htsmd5.h \
htsmodules.h htsname.h htsnet.h htssniff.h \
htsopt.h htsrobots.h htsthread.h \
htstools.h htswizard.h htswrap.h htscodec.h htswarc.h htsproxy.h htszlib.h \
htstools.h htswizard.h htswrap.h htscodec.h htswarc.h htschanges.h htssinglefile.h htssitemap.h htsproxy.h htszlib.h \
htsstrings.h htsarrays.h httrack-library.h \
htscharset.h punycode.h htsencoding.h \
htsentities.h htsentities.sh htsbasiccharsets.sh htscodepages.h \
@@ -84,7 +87,7 @@ libhttrack_la_LIBADD = $(THREADS_LIBS) $(ZLIB_LIBS) $(BROTLI_LIBS) $(ZSTD_LIBS)
libhttrack_la_CFLAGS = $(AM_CFLAGS) -DLIBHTTRACK_EXPORTS -DZLIB_CONST
libhttrack_la_LDFLAGS = $(AM_LDFLAGS) -version-info $(VERSION_INFO)
EXTRA_DIST = httrack.h webhttrack \
EXTRA_DIST = httrack.h htsstats.h webhttrack \
version.rc \
libhttrack.rc \
httrack.rc \

View File

@@ -487,141 +487,6 @@ regen:
#define HTS_DATA_UNKNOWN_HTML_LEN 0
#define HTS_DATA_ERROR_HTML "<html>"LF\
"<html xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"en\">"LF\
""LF\
"<head>"LF\
" <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />"LF\
" <meta name=\"description\" content=\"HTTrack is an easy-to-use website mirror utility. It allows you to download a World Wide website from the Internet to a local directory,building recursively all structures, getting html, images, and other files from the server to your computer. Links are rebuiltrelatively so that you can freely browse to the local site (works with any browser). You can mirror several sites together so that you can jump from one toanother. You can, also, update an existing mirror site, or resume an interrupted download. The robot is fully configurable, with an integrated help\" />"LF\
" <meta name=\"keywords\" content=\"httrack, HTTRACK, HTTrack, winhttrack, WINHTTRACK, WinHTTrack, offline browser, web mirror utility, aspirateur web, surf offline, web capture, www mirror utility, browse offline, local site builder, website mirroring, aspirateur www, internet grabber, capture de site web, internet tool, hors connexion, unix, dos, windows 95, windows 98, solaris, ibm580, AIX 4.0, HTS, HTGet, web aspirator, web aspirateur, libre, GPL, GNU, free software\" />"LF\
" <title>Page not retrieved! - HTTrack Website Copier</title>"LF\
" <style type=\"text/css\">"LF\
" <!--"LF\
""LF\
"body {"LF\
" margin: 0; padding: 0; margin-bottom: 15px; margin-top: 8px;"LF\
" background: #77b;"LF\
"}"LF\
"body, td {"LF\
" font: 14px \"Trebuchet MS\", Verdana, Arial, Helvetica, sans-serif;"LF\
" }"LF\
""LF\
"#subTitle {"LF\
" background: #000; color: #fff; padding: 4px; font-weight: bold; "LF\
" }"LF\
""LF\
"#siteNavigation a, #siteNavigation .current {"LF\
" font-weight: bold; color: #448;"LF\
" }"LF\
"#siteNavigation a:link { text-decoration: none; }"LF\
"#siteNavigation a:visited { text-decoration: none; }"LF\
""LF\
"#siteNavigation .current { background-color: #ccd; }"LF\
""LF\
"#siteNavigation a:hover { text-decoration: none; background-color: #fff; color: #000; }"LF\
"#siteNavigation a:active { text-decoration: none; background-color: #ccc; }"LF\
""LF\
""LF\
"a:link { text-decoration: underline; color: #00f; }"LF\
"a:visited { text-decoration: underline; color: #000; }"LF\
"a:hover { text-decoration: underline; color: #c00; }"LF\
"a:active { text-decoration: underline; }"LF\
""LF\
"#pageContent {"LF\
" clear: both;"LF\
" border-bottom: 6px solid #000;"LF\
" padding: 10px; padding-top: 20px;"LF\
" line-height: 1.65em;"LF\
" background-image: url(backblue.gif);"LF\
" background-repeat: no-repeat;"LF\
" background-position: top right;"LF\
" }"LF\
""LF\
"#pageContent, #siteNavigation {"LF\
" background-color: #ccd;"LF\
" }"LF\
""LF\
""LF\
".imgLeft { float: left; margin-right: 10px; margin-bottom: 10px; }"LF\
".imgRight { float: right; margin-left: 10px; margin-bottom: 10px; }"LF\
""LF\
"hr { height: 1px; color: #000; background-color: #000; margin-bottom: 15px; }"LF\
""LF\
"h1 { margin: 0; font-weight: bold; font-size: 2em; }"LF\
"h2 { margin: 0; font-weight: bold; font-size: 1.6em; }"LF\
"h3 { margin: 0; font-weight: bold; font-size: 1.3em; }"LF\
"h4 { margin: 0; font-weight: bold; font-size: 1.18em; }"LF\
""LF\
".blak { background-color: #000; }"LF\
".hide { display: none; }"LF\
".tableWidth { min-width: 400px; }"LF\
""LF\
".tblRegular { border-collapse: collapse; }"LF\
".tblRegular td { padding: 6px; background-image: url(fade.gif); border: 2px solid #99c; }"LF\
".tblHeaderColor, .tblHeaderColor td { background: #99c; }"LF\
".tblNoBorder td { border: 0; }"LF\
""LF\
""LF\
"// -->"LF\
"</style>"LF\
""LF\
"</head>"LF\
""LF\
"<table width=\"76%%\" border=\"0\" align=\"center\" cellspacing=\"0\" cellpadding=\"3\" class=\"tableWidth\">"LF\
" <tr>"LF\
" <td id=\"subTitle\">HTTrack Website Copier - Open Source offline browser</td>"LF\
" </tr>"LF\
"</table>"LF\
"<table width=\"76%%\" border=\"0\" align=\"center\" cellspacing=\"0\" cellpadding=\"0\" class=\"tableWidth\">"LF\
"<tr class=\"blak\">"LF\
"<td>"LF\
" <table width=\"100%%\" border=\"0\" align=\"center\" cellspacing=\"1\" cellpadding=\"0\">"LF\
" <tr>"LF\
" <td colspan=\"6\"> "LF\
" <table width=\"100%%\" border=\"0\" align=\"center\" cellspacing=\"0\" cellpadding=\"10\">"LF\
" <tr> "LF\
" <td id=\"pageContent\"> "LF\
"<!-- ==================== End prologue ==================== -->"LF\
"<h1><strong><u>Oops!...</u></strong></h1>"LF\
"<h3>This page has <font color=\"red\"><em>not</em></font> been retrieved by HTTrack Website Copier (%s). </h3>"LF\
"<script language=\"Javascript\">"LF\
"<!--"LF\
" var loc=document.location.toString();"LF\
" if (loc) {"LF\
" var pos=loc.indexOf('link=');"LF\
" if (pos>0) {"LF\
" document.write('Clic to the link <b>below</b> to go to the online location!<br><a href=\"'+loc.substring(pos+5)+'\">'+loc.substring(pos+5)+'</a><br>');"LF\
" } else"LF\
" document.write('(no location defined)');"LF\
" }"LF\
"// -->"LF\
"</script>"LF\
"<h6 align=\"right\">Mirror by HTTrack Website Copier</h6>"LF\
"</body>"LF\
"</html>"LF\
"<!-- ==================== Start epilogue ==================== -->"LF\
" </td>"LF\
" </tr>"LF\
" </table>"LF\
" </td>"LF\
" </tr>"LF\
" </table>"LF\
"</td>"LF\
"</tr>"LF\
"</table>"LF\
""LF\
"<table width=\"76%%\" height=\"100%%\" border=\"0\" align=\"center\" valign=\"bottom\" cellspacing=\"0\" cellpadding=\"0\">"LF\
" <tr>"LF\
" <td id=\"footer\"><small>&copy; 2014 Xavier Roche & other contributors - Web Design: Kauler Leto.</small></td>"LF\
" </tr>"LF\
"</table>"LF\
""LF\
"</body>"LF\
""LF\
"</html>"LF\
""LF\
""LF
// image gif "unknown"
#define HTS_DATA_UNKNOWN_GIF \
"\x47\x49\x46\x38\x39\x61\x20\x0\x20\x0\xf7\xff\x0\xc0\xc0\xc0\xff\x0\x0\xfc\x3\x0\xf8\x6\x0\xf6\x9\x0\xf2\xc\x0\xf0\xf\x0\xf0\xe\x0\xed\x11\x0\xec\x13\x0\xeb\x14\x0\xe9\x15\x0\xe8\x18\x0\xe6\x18\x0\xe5\x1a\x0\xe3\x1c\x0\xe2\x1d\x0\xe1\x1e\x0\xdf\x20\x0\xdd\x23\x0\xdd\x22\x0\xdb\x23\x0\xda\x25\x0\xd9\x25\x0\xd8\x27\x0\xd6\x29\x0\xd5\x2a\x0\xd3\x2c\x0\xd2\x2d\x0"\

View File

@@ -114,6 +114,12 @@ const char *hts_optalias[][4] = {
"strip [host/pattern=]key1,key2,... from URLs"},
{"cookies-file", "-%K", "param1",
"load extra cookies from a Netscape cookies.txt"},
{"changes", "-%d", "single",
"write hts-changes.json: what this crawl changed vs. the previous mirror"},
{"sitemap", "-%m", "single",
"seed the crawl from the start host's sitemap (robots.txt, then "
"/sitemap.xml)"},
{"sitemap-url", "-%mu", "param1", "seed the crawl from this sitemap URL"},
{"warc", "-%r", "single", "write an ISO-28500 WARC/1.1 archive of the crawl"},
{"warc-file", "-%rf", "param1", "write a WARC archive to the given base name"},
{"warc-max-size", "-%rs", "param1",
@@ -123,6 +129,10 @@ const char *hts_optalias[][4] = {
{"warc-cdxj", "-%rc", "single", ""},
{"wacz", "-%rz", "single",
"package the WARC archive, CDXJ index and pages as a WACZ file"},
{"single-file", "-%Z", "single",
"after the mirror, inline each page's assets as data: URIs"},
{"single-file-max-size", "-%Zs", "param1",
"per-asset cap for --single-file, in bytes (implies it; default 10485760)"},
{"why", "-%Y", "param1",
"explain which filter rule accepts or rejects a URL, then exit"},
{"pause", "-%G", "param1",

View File

@@ -38,6 +38,7 @@ Please visit our Website: http://www.httrack.com
#include "htsnet.h"
#include "htscore.h"
#include "htswarc.h"
#include "htschanges.h"
#include "htsthread.h"
#include <time.h>
/* END specific definitions */
@@ -47,11 +48,6 @@ Please visit our Website: http://www.httrack.com
#include "htsftp.h"
#include "htscodec.h"
#include "htsproxy.h"
#if HTS_USEZLIB
#include "htszlib.h"
#else
#error HTS_USEZLIB not defined
#endif
#ifdef _WIN32
#ifndef __cplusplus
@@ -125,6 +121,22 @@ void back_free(struct_back ** sback) {
above a normal handshake. The last candidate still gets the full timeout. */
#define HTS_CONNECT_FALLBACK_TIMEOUT 10
void back_read_ftp_result(FILE *fp, htsblk *r) {
size_t j = 0;
if (fscanf(fp, "%d ", &r->statuscode) != 1)
r->statuscode = STATUSCODE_INVALID;
// an external helper writes this file: stop at capacity, not at EOF
while (j + 1 < sizeof(r->msg)) {
const int c = fgetc(fp);
if (c == EOF)
break;
r->msg[j++] = (char) c;
}
r->msg[j] = '\0';
}
int back_connect_fallback_due(int addr_index, int addr_count, int elapsed,
int timeout) {
int deadline;
@@ -328,12 +340,111 @@ static int back_index_ready(httrackp * opt, struct_back * sback, const char *adr
}
static int slot_can_be_cached_on_disk(const lien_back * back) {
/* A pending backup or spool means the slot is not finalized, and the swap
would unlink it through back_clear_entry() (#771). */
if (back->tmpfile != NULL && back->tmpfile[0] != '\0')
return 0;
return (back->status == STATUS_READY && back->locked == 0
&& back->url_sav[0] != '\0'
&& strcmp(back->url_sav, BACK_ADD_TEST) != 0);
/* Note: not checking !IS_DELAYED_EXT(back->url_sav) or it will quickly cause the slots to be filled! */
}
int back_selftest_slot_swap(void) {
lien_back back;
int err = 0;
#define CHECK(want, why) \
do { \
if (slot_can_be_cached_on_disk(&back) != (want)) { \
fprintf(stderr, "backswap: expected %d for %s\n", (want), (why)); \
err = 1; \
} \
} while (0)
memset(&back, 0, sizeof(back));
back.status = STATUS_READY;
strcpybuff(back.url_sav, "/tmp/httrack-selftest.bin");
CHECK(1, "a plain ready slot");
back.tmpfile = back.tmpfile_buffer;
strcpybuff(back.tmpfile_buffer, "/tmp/httrack-selftest.bin.bak");
CHECK(0, "a slot still holding a re-fetch backup");
/* Callers clear a spent temporary by emptying the name, not the pointer. */
back.tmpfile_buffer[0] = '\0';
CHECK(1, "a slot whose temporary was already dropped");
back.tmpfile = NULL;
back.locked = 1;
CHECK(0, "a locked slot");
back.locked = 0;
back.status = STATUS_TRANSFER;
CHECK(0, "a slot still transferring");
back.status = STATUS_READY;
back.url_sav[0] = '\0';
CHECK(0, "a slot with no save name");
strcpybuff(back.url_sav, BACK_ADD_TEST);
CHECK(0, "the dummy test slot");
#undef CHECK
/* The swap round-trip must not lose the size of a slot whose body is already
at url_sav, or the link writer blanks the file (#797). */
{
static const char body[] = "swapped body";
int c;
for (c = 0; c < 2; c++) {
const hts_boolean inmemory = c == 0 ? HTS_TRUE : HTS_FALSE;
FILE *const fp = tmpfile();
lien_back *copy = NULL;
memset(&back, 0, sizeof(back));
back.status = STATUS_READY;
strcpybuff(back.url_sav, "/tmp/httrack-selftest.bin");
back.r.size = (LLint) sizeof(body) - 1;
if (inmemory) {
back.r.adr = strdupt(body);
}
if (fp == NULL || back_serialize(fp, &back) != 0 ||
fseek(fp, 0, SEEK_SET) != 0 || back_unserialize(fp, &copy) != 0) {
fprintf(stderr, "backswap: round-trip failed for a %s slot\n",
inmemory ? "buffered" : "direct-to-disk");
err = 1;
} else {
if (copy->r.size != back.r.size) {
fprintf(stderr,
"backswap: %s slot came back with size " LLintP
", expected " LLintP "\n",
inmemory ? "buffered" : "direct-to-disk", copy->r.size,
back.r.size);
err = 1;
}
if (inmemory && (copy->r.adr == NULL ||
memcmp(copy->r.adr, body, sizeof(body) - 1) != 0)) {
fprintf(stderr, "backswap: buffered slot lost its body\n");
err = 1;
}
if (!inmemory && copy->r.adr != NULL) {
fprintf(stderr, "backswap: direct-to-disk slot gained a body\n");
err = 1;
}
back_clear_entry(copy);
freet(copy);
}
if (fp != NULL)
fclose(fp);
freet(back.r.adr);
}
}
printf("backswap self-test: %s\n", err ? "FAIL" : "OK");
return err;
}
/* Put all backing entries that are ready in the storage hashtable to spare space and CPU */
int back_cleanup_background(httrackp * opt, cache_back * cache,
struct_back * sback) {
@@ -542,8 +653,14 @@ static int create_back_tmpfile(httrackp *opt, lien_back *const back,
// do not use tempnam() but a regular filename
back->tmpfile_buffer[0] = '\0';
if (back->url_sav[0] != '\0') {
snprintf(back->tmpfile_buffer, sizeof(back->tmpfile_buffer), "%s.%s",
back->url_sav, ext);
/* same capacity as url_sav, so truncation drops the extension and aliases
the temp name onto the live file that back_finalize_backup() UNLINKs */
if (!sprintfbuff(back->tmpfile_buffer, "%s.%s", back->url_sav, ext)) {
hts_log_print(opt, LOG_WARNING, "temporary filename too long for %s",
back->url_sav);
back->tmpfile_buffer[0] = '\0';
return -1;
}
back->tmpfile = back->tmpfile_buffer;
if (structcheck(back->tmpfile) != 0) {
hts_log_print(opt, LOG_WARNING, "can not create directory to %s",
@@ -551,8 +668,15 @@ static int create_back_tmpfile(httrackp *opt, lien_back *const back,
return -1;
}
} else {
snprintf(back->tmpfile_buffer, sizeof(back->tmpfile_buffer), "%s/tmp%d.%s",
StringBuff(opt->path_html_utf8), opt->state.tmpnameid++, ext);
/* truncation here would collide distinct tmpnameid's onto one name */
if (!sprintfbuff(back->tmpfile_buffer, "%s/tmp%d.%s",
StringBuff(opt->path_html_utf8), opt->state.tmpnameid++,
ext)) {
hts_log_print(opt, LOG_WARNING, "temporary filename too long in %s",
StringBuff(opt->path_html_utf8));
back->tmpfile_buffer[0] = '\0';
return -1;
}
back->tmpfile = back->tmpfile_buffer;
}
/* OK */
@@ -560,12 +684,40 @@ static int create_back_tmpfile(httrackp *opt, lien_back *const back,
return 0;
}
/* Move src onto dst; RENAME does not clobber an existing target on Windows. */
static hts_boolean replace_file(const char *src, const char *dst) {
if (RENAME(src, dst) == 0)
return HTS_TRUE;
(void) UNLINK(dst);
return RENAME(src, dst) == 0 ? HTS_TRUE : HTS_FALSE;
/* Note: utf-8 */
void back_refetch_backup(httrackp *opt, lien_back *const back) {
back->tmpfile = NULL;
if (fexist_utf8(back->url_sav)) {
hts_boolean saved = HTS_FALSE;
if (create_back_tmpfile(opt, back, "bak") == 0) {
/* clobber a .bak a killed run left behind, or the guard stays off for
good (#758) */
if (fexist_utf8(back->tmpfile))
hts_log_print(opt, LOG_WARNING, "replacing leftover backup %s",
back->tmpfile);
saved = hts_rename_over(opt, back->url_sav, back->tmpfile);
}
if (!saved) {
hts_log_print(opt, LOG_WARNING | LOG_ERRNO,
"could not back up %s; an aborted re-fetch will lose it",
back->url_sav);
back->tmpfile = NULL;
}
}
}
/* Did the fetch fail to produce a response, as opposed to the engine
deliberately passing the resource over? Only the latter may be purged. */
static hts_boolean back_transfer_failed(const int statuscode) {
switch (statuscode) {
case STATUSCODE_TOO_BIG:
case STATUSCODE_EXCLUDED:
case STATUSCODE_TEST_OK:
return HTS_FALSE;
default:
return statuscode <= 0 ? HTS_TRUE : HTS_FALSE;
}
}
/* Commit or restore a re-fetch backup (#77 follow-up): a re-fetch over an
@@ -585,7 +737,7 @@ static void back_finalize_backup(httrackp *opt, lien_back *const back,
}
/* On failure keep the backup: an orphaned temp beats losing the good copy.
*/
if (!replace_file(back->tmpfile, back->url_sav))
if (!hts_rename_over(opt, back->tmpfile, back->url_sav))
hts_log_print(opt, LOG_WARNING | LOG_ERRNO,
"could not restore %s; previous copy kept as %s",
back->url_sav, back->tmpfile);
@@ -699,6 +851,13 @@ int back_finalize(httrackp * opt, cache_back * cache, struct_back * sback,
if ((size = hts_codec_unpack(codec, back[p].tmpfile,
unpacked)) >= 0) {
back[p].r.size = back[p].r.totalsize = size;
if (back[p].r.is_write) {
/* Sample the previous copy now: the rename below replaces
it, and file_notify() only fires once it is gone. */
hts_changes_notify(
opt, back[p].url_adr, back[p].url_fil, back[p].url_sav,
HTS_TRUE, back[p].r.notmodified ? HTS_TRUE : HTS_FALSE);
}
if (!back[p].r.is_write) {
// fichier -> mémoire ; le fichier est écrit plus tard
deleteaddr(&back[p].r);
@@ -709,7 +868,7 @@ int back_finalize(httrackp * opt, cache_back * cache, struct_back * sback,
"Read error when decompressing");
}
UNLINK(unpacked);
} else if (replace_file(unpacked, back[p].url_sav)) {
} else if (hts_rename_over(opt, unpacked, back[p].url_sav)) {
/* The temp bypassed filecreate(), which is what chmods. */
#ifndef _WIN32
chmod(back[p].url_sav, HTS_ACCESS_FILE);
@@ -1007,6 +1166,14 @@ int back_finalize(httrackp * opt, cache_back * cache, struct_back * sback,
/* Aborted, error, or not ready: url_sav (if written) is broken; restore the
previous copy from the backup. */
back_finalize_backup(opt, &back[p], HTS_FALSE);
/* Note the surviving copy, or the end-of-update purge drops what this run
never managed to replace (#746). */
if (!back[p].testmode && back_transfer_failed(back[p].r.statuscode) &&
back[p].url_sav[0] != '\0' && fexist_utf8(back[p].url_sav)) {
filenote(&opt->state.strc, back[p].url_sav, NULL);
file_notify(opt, back[p].url_adr, back[p].url_fil, back[p].url_sav, 0, 0,
back[p].r.notmodified);
}
return -1;
}
@@ -1056,6 +1223,13 @@ void back_connxfr(htsblk * src, htsblk * dst) {
src->debugid = 0;
}
/* Release the buffers a response owns. The connection members are left alone:
back_connxfr() moves those, and the file handles are closed elsewhere. */
static void back_free_response(htsblk *r) {
deleteaddr(r);
warc_free_request(r);
}
void back_move(lien_back * src, lien_back * dst) {
memcpy(dst, src, sizeof(lien_back));
memset(src, 0, sizeof(lien_back));
@@ -1149,7 +1323,10 @@ int back_unserialize(FILE * fp, lien_back ** dst) {
(*dst)->r.ssl_con = NULL;
#endif
if (back_data_unserialize(fp, (void **) &(*dst)->r.adr, &size) == 0) {
(*dst)->r.size = size;
/* A bodyless slot already wrote its bytes to url_sav (FTP, direct to
disk); zeroing r.size makes the writer blank that file (#797). */
if ((*dst)->r.adr != NULL)
(*dst)->r.size = size;
(*dst)->r.headers = NULL;
if (back_string_unserialize(fp, &(*dst)->r.headers) == 0)
return 0; /* ok */
@@ -1588,10 +1765,7 @@ int back_clear_entry(lien_back * back) {
back->r.soc = INVALID_SOCKET;
}
if (back->r.adr != NULL) { // reste un bloc à désallouer
freet(back->r.adr);
back->r.adr = NULL;
}
back_free_response(&back->r);
if (back->chunk_adr != NULL) { // reste un bloc à désallouer
freet(back->chunk_adr);
back->chunk_adr = NULL;
@@ -1604,12 +1778,6 @@ int back_clear_entry(lien_back * back) {
(void) unlink(back->tmpfile);
back->tmpfile = NULL;
}
// headers
if (back->r.headers != NULL) {
freet(back->r.headers);
back->r.headers = NULL;
}
warc_free_request(&back->r);
// Tout nettoyer
memset(back, 0, sizeof(lien_back));
back->r.soc = INVALID_SOCKET;
@@ -2947,7 +3115,7 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
back[i].r.msg[0] = '\0';
strncatbuff(back[i].r.msg, tmp, sizeof(back[i].r.msg) - 2);
if (!strnotempty(back[i].r.msg)) {
sprintf(back[i].r.msg, "SSL/TLS error %d", err_code);
htsblk_failf(&back[i].r, "SSL/TLS error %d", err_code);
}
deletehttp(&back[i].r);
back[i].r.soc = INVALID_SOCKET;
@@ -3020,16 +3188,7 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
FOPEN(fconcat(OPT_GET_BUFF(opt), back[i].location_buffer, ".ok"),
"rb");
if (fp) {
int j = 0;
fscanf(fp, "%d ", &(back[i].r.statuscode));
while(!feof(fp)) {
int c = fgetc(fp);
if (c != EOF)
back[i].r.msg[j++] = c;
}
back[i].r.msg[j++] = '\0';
back_read_ftp_result(fp, &back[i].r);
fclose(fp);
UNLINK(fconcat(OPT_GET_BUFF(opt), back[i].location_buffer, ".ok"));
strcpybuff(fconcat
@@ -3128,20 +3287,7 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
back[i].url_sav, 1, 1,
back[i].r.notmodified);
back[i].r.compressed = 0;
/* Re-fetch over an existing file (#77 follow-up):
move the good copy aside before truncating it
so an aborted transfer can restore it. url_sav
is still written normally (file list intact).
*/
back[i].tmpfile = NULL;
if (fexist_utf8(back[i].url_sav)) {
if (create_back_tmpfile(opt, &back[i], "bak") !=
0 ||
RENAME(back[i].url_sav, back[i].tmpfile) !=
0) {
back[i].tmpfile = NULL;
}
}
back_refetch_backup(opt, &back[i]);
if ((back[i].r.out =
filecreate(&opt->state.strc,
back[i].url_sav)) == NULL) {
@@ -3330,10 +3476,11 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
deleteaddr(&back[i].r);
if (back[i].r.size < back[i].r.totalsize)
back[i].r.statuscode = STATUSCODE_CONNERROR; // recatch
sprintf(back[i].r.msg,
"Incorrect length (" LLintP " Bytes, " LLintP
" expected)", (LLint) back[i].r.size,
(LLint) back[i].r.totalsize);
htsblk_failf(&back[i].r,
"Incorrect length (" LLintP " Bytes, " LLintP
" expected)",
(LLint) back[i].r.size,
(LLint) back[i].r.totalsize);
} else {
// Un warning suffira..
hts_log_print(opt, LOG_WARNING,
@@ -3935,6 +4082,9 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
memset(&tmp, 0, sizeof(tmp));
back_connxfr(&back[i].r, &tmp);
/* the cache entry overwrites the whole struct, so drop
what the 304 response still owns first (#782) */
back_free_response(&back[i].r);
back[i].r =
cache_read(opt, cache, back[i].url_adr, back[i].url_fil,
back[i].url_sav, back[i].location_buffer);

View File

@@ -74,6 +74,10 @@ void back_free(struct_back ** sback);
// backing
#define BACK_ADD_TEST "(dummy)"
#define BACK_ADD_TEST2 "(dummy2)"
/* Parse an external FTP helper's "<statuscode> <message>" result file into r,
clipping the message to r->msg. */
void back_read_ftp_result(FILE *fp, htsblk *r);
int back_index(httrackp * opt, struct_back * sback, const char *adr, const char *fil,
const char *sav);
int back_available(const struct_back * sback);
@@ -135,6 +139,12 @@ int back_trylive(httrackp * opt, cache_back * cache, struct_back * sback,
const int p);
int back_finalize(httrackp * opt, cache_back * cache, struct_back * sback,
const int p);
/* Move the previous copy of back->url_sav to back->tmpfile so back_finalize()
can put it back when the re-fetch fails (#77 follow-up). Call right before
truncating url_sav; tmpfile stays NULL when there is nothing to save. */
void back_refetch_backup(httrackp *opt, lien_back *const back);
/* -#test=backswap: slots eligible for the on-disk ready table. */
int back_selftest_slot_swap(void);
void back_info(struct_back * sback, int i, int j, FILE * fp);
void back_infostr(struct_back *sback, int i, int j, char *s, size_t size);
LLint back_transferred(LLint add, struct_back * sback);

238
src/htsbacktrace.c Normal file
View File

@@ -0,0 +1,238 @@
/* ------------------------------------------------------------ */
/*
HTTrack Website Copier, Offline Browser for Windows and Unix
Copyright (C) 1998 Xavier Roche and other contributors
SPDX-License-Identifier: GPL-3.0-or-later
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Ethical use: we kindly ask that you NOT use this software to harvest email
addresses or to collect any other private information about people. Doing so
would dishonor our work and waste the many hours we have spent on it.
Please visit our Website: http://www.httrack.com
*/
/* ------------------------------------------------------------ */
/* File: crash backtrace printer */
/* Author: Xavier Roche */
/* ------------------------------------------------------------ */
/* Before every header: glibc gates dladdr() on it. */
#if defined(__linux) && !defined(_GNU_SOURCE)
#define _GNU_SOURCE
#endif
#include "htsbacktrace.h"
#include "htsglobal.h"
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#include <io.h> /* write */
#endif
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#if (defined(__linux) && defined(HAVE_EXECINFO_H))
#include <dlfcn.h>
#include <errno.h>
#include <execinfo.h>
#include <signal.h>
#include <time.h>
#include <sys/wait.h>
#define USES_BACKTRACE
#endif
#ifdef USES_BACKTRACE
#define BT_MAX_FRAMES 64 /* frames we try to name */
#define BT_MAX_MODULES 8 /* distinct modules, one child each */
#define BT_HEX_SIZE 19 /* "0x" + 16 nibbles + NUL */
#define BT_PATH_SIZE 1024 /* module path; longer is skipped */
#define BT_WAIT_TICKS 300 /* 10ms ticks, shared: cap a slow child */
#define BT_NO_SYMBOLIZER 127 /* child exit: execvp() found none */
static hts_boolean symbolize_crash = HTS_TRUE;
/* "0x"-prefixed hex: the handler must stay stdio-free. */
static void print_hex(char *buffer, uintptr_t value) {
static const char digits[] = "0123456789abcdef";
size_t i = 2, a, b;
buffer[0] = '0';
buffer[1] = 'x';
do {
buffer[i++] = digits[value & 0xf];
value >>= 4;
} while (value != 0);
buffer[i] = '\0';
for (a = 2, b = i - 1; a < b; a++, b--) {
const char c = buffer[a];
buffer[a] = buffer[b];
buffer[b] = c;
}
}
/* HTS_FALSE if src does not fit: a truncated module path would point the
symbolizer at the wrong file. */
static hts_boolean copy_bounded(char *dest, size_t size, const char *src) {
size_t i;
for (i = 0; i < size - 1 && src[i] != '\0'; i++) {
dest[i] = src[i];
}
dest[i] = '\0';
return src[i] == '\0' ? HTS_TRUE : HTS_FALSE;
}
/* Run the symbolizer on argv, output on fd, within *budget ticks. HTS_FALSE
only if none could be run at all; otherwise silent, the raw trace stands. */
static hts_boolean spawn_symbolizer(char **argv, int fd, int *budget) {
const pid_t pid = fork();
int status = 0;
if (pid == -1)
return HTS_FALSE;
if (pid == 0) {
static char llvm_prog[] = "llvm-symbolizer";
static char llvm_opts[] = "-p";
dup2(fd, 1); /* both symbolizers write on stdout */
execvp(argv[0], argv);
argv[0] = llvm_prog; /* an LLVM-only install ships no addr2line */
argv[1] = llvm_opts;
execvp(argv[0], argv);
_exit(BT_NO_SYMBOLIZER);
}
for (; *budget > 0; (*budget)--) {
const struct timespec tick = {0, 10 * 1000 * 1000};
const pid_t reaped = waitpid(pid, &status, WNOHANG);
if (reaped == pid)
return WIFEXITED(status) && WEXITSTATUS(status) == BT_NO_SYMBOLIZER
? HTS_FALSE
: HTS_TRUE;
if (reaped == -1 && errno != EINTR)
return HTS_TRUE;
nanosleep(&tick, NULL);
}
kill(pid, SIGKILL);
waitpid(pid, NULL, 0);
return HTS_TRUE;
}
/* Name the frames backtrace_symbols_fd() leaves as module+offset:
-fvisibility=hidden keeps them out of .dynsym, but DWARF has them. dladdr()
is not formally async-signal-safe; accepted, this path is already fatal. */
static void symbolize_backtrace(void *const *stack, int size, int fd) {
static char prog[] = "addr2line";
static char opts[] = "-Cfipa";
static char dashe[] = "-e";
char hex[BT_MAX_FRAMES][BT_HEX_SIZE];
const void *base[BT_MAX_FRAMES];
const char *name[BT_MAX_FRAMES];
hts_boolean grouped[BT_MAX_FRAMES];
char module[BT_PATH_SIZE];
char *argv[4 + BT_MAX_FRAMES + 1];
int budget = BT_WAIT_TICKS;
int i, spawned;
if (size > BT_MAX_FRAMES)
size = BT_MAX_FRAMES;
for (i = 0; i < size; i++) {
Dl_info info;
grouped[i] = HTS_TRUE; /* skipped unless dladdr() places the frame */
if (dladdr(stack[i], &info) == 0 || info.dli_fname == NULL ||
info.dli_fname[0] == '\0')
continue;
base[i] = info.dli_fbase;
name[i] = info.dli_fname;
print_hex(hex[i], (uintptr_t) ((const char *) stack[i] -
(const char *) info.dli_fbase));
grouped[i] = HTS_FALSE;
}
/* One child per module: addr2line takes a single -e. Each frame is claimed
once, so argc cannot exceed argv[]. */
for (spawned = 0; spawned < BT_MAX_MODULES; spawned++) {
int first, j, argc = 0;
for (first = 0; first < size && grouped[first]; first++)
;
if (first >= size)
break;
argv[argc++] = prog;
argv[argc++] = opts;
argv[argc++] = dashe;
argv[argc++] = module;
for (j = first; j < size; j++) {
if (grouped[j] || base[j] != base[first])
continue;
grouped[j] = HTS_TRUE;
argv[argc++] = hex[j];
}
argv[argc] = NULL;
/* access(): skip pseudo-modules like linux-vdso, which have no file and
would draw nothing but an addr2line complaint. */
if (copy_bounded(module, sizeof(module), name[first]) &&
access(module, R_OK) == 0) {
const size_t len = strlen(module);
/* addr2line -a prints offsets only: say which module they are in. */
(void) (write(fd, module, len) == (ssize_t) len);
(void) (write(fd, ":\n", 2) == 2);
if (!spawn_symbolizer(argv, fd, &budget))
break; /* no symbolizer: stop at one header */
}
}
}
#endif
void hts_backtrace_init(void) {
#ifdef USES_BACKTRACE
symbolize_crash =
getenv("HTTRACK_NO_SYMBOLIZE") == NULL ? HTS_TRUE : HTS_FALSE;
#endif
}
void hts_print_backtrace(int fd) {
#ifdef USES_BACKTRACE
void *stack[256];
const int size = backtrace(stack, sizeof(stack) / sizeof(stack[0]));
/* A fault inside the handler lands back here: symbolizing twice interleaves
two traces on fd and spends a second budget. */
static volatile sig_atomic_t entered = 0;
if (size != 0) {
backtrace_symbols_fd(stack, size, fd);
if (symbolize_crash && entered == 0) {
entered = 1;
symbolize_backtrace(stack, size, fd);
entered = 0;
}
}
#else
const char msg[] = "No stack trace available on this OS :(\n";
if (write(fd, msg, sizeof(msg) - 1) != sizeof(msg) - 1) {
/* sorry GCC */
}
#endif
}

45
src/htsbacktrace.h Normal file
View File

@@ -0,0 +1,45 @@
/* ------------------------------------------------------------ */
/*
HTTrack Website Copier, Offline Browser for Windows and Unix
Copyright (C) 1998 Xavier Roche and other contributors
SPDX-License-Identifier: GPL-3.0-or-later
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Ethical use: we kindly ask that you NOT use this software to harvest email
addresses or to collect any other private information about people. Doing so
would dishonor our work and waste the many hours we have spent on it.
Please visit our Website: http://www.httrack.com
*/
/* ------------------------------------------------------------ */
/* File: crash backtrace printer */
/* Author: Xavier Roche */
/* ------------------------------------------------------------ */
#ifndef HTSBACKTRACE_DEFH
#define HTSBACKTRACE_DEFH
/* Sample HTTRACK_NO_SYMBOLIZE before any crash: getenv() is not signal-safe.
Call once, from the process that installs the fatal-signal handlers. */
void hts_backtrace_init(void);
/* Write the calling thread's stack to fd, callable from a fatal signal handler:
raw frames first, then whatever an external symbolizer can name. Allocates
nothing; prints a one-line notice where the OS has no backtrace(). */
void hts_print_backtrace(int fd);
#endif

View File

@@ -142,10 +142,12 @@ struct cache_back_zip_entry {
int compressionMethod;
};
/* A corrupt cache can carry a field wider than ours; clipping it keeps the
entry, where aborting would take the crawl down. */
#define ZIP_READFIELD_STRING(line, value, refline, refvalue, refvalue_size) \
do { \
if (line[0] != '\0' && strfield2(line, refline)) { \
strlcpybuff(refvalue, value, refvalue_size); \
(void) strclipbuff(refvalue, refvalue_size, value); \
line[0] = '\0'; \
} \
} while (0)
@@ -853,13 +855,6 @@ static htsblk cache_readex_new(httrackp * opt, cache_back * cache,
return r;
}
// lecture d'un fichier dans le cache
// si save==null alors test unqiquement
static int hts_rename(httrackp * opt, const char *a, const char *b) {
hts_log_print(opt, LOG_DEBUG, "Cache: rename %s -> %s (%p %p)", a, b, a, b);
return RENAME(a, b);
}
/* Open the cache ZIP via hts_fopen_utf8 so a non-ASCII path_log isn't mangled
to ANSI (#630); 64-bit funcs keep multi-GB caches whole on Windows LLP64. */
static voidpf ZCALLBACK hts_zip_fopen_utf8(voidpf opaque, const void *filename,
@@ -989,29 +984,16 @@ void cache_init(cache_back * cache, httrackp * opt) {
OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
StringBuff(opt->path_log),
"hts-cache/new.zip")))) { // a previous cache exists.. rename it
/* Remove OLD cache */
if (fexist_utf8(fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
StringBuff(opt->path_log),
"hts-cache/old.zip"))) {
if (UNLINK(fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
StringBuff(opt->path_log), "hts-cache/old.zip")) !=
0) {
hts_log_print(opt, LOG_WARNING | LOG_ERRNO,
"Cache: error while moving previous cache");
}
}
/* Rename */
if (hts_rename
(opt,
fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt), StringBuff(opt->path_log),
"hts-cache/new.zip"), fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
StringBuff(opt->path_log),
"hts-cache/old.zip")) != 0) {
if (!hts_rename_over(
opt,
fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
StringBuff(opt->path_log), "hts-cache/new.zip"),
fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
StringBuff(opt->path_log), "hts-cache/old.zip"))) {
hts_log_print(opt, LOG_WARNING | LOG_ERRNO,
"Cache: error while moving previous cache");
} else {
hts_log_print(opt, LOG_DEBUG, "Cache: successfully renamed");
hts_log_print(opt, LOG_DEBUG, "Cache: rotated new.zip to old.zip");
}
}
} else {
@@ -1295,12 +1277,17 @@ char *readfile2(const char *fil, LLint * size) {
}
/* Note: utf-8 */
char *readfile_utf8(const char *fil) {
char *readfile_utf8(const char *fil) { return readfile2_utf8(fil, NULL); }
/* Note: utf-8 */
char *readfile2_utf8(const char *fil, LLint *size) {
char *adr = NULL;
char catbuff[CATBUFF_SIZE];
const LLint len = fsize_utf8(fil);
const size_t buflen = len >= 0 ? llint_to_size_t(len) : (size_t) -1;
if (size != NULL)
*size = len;
if (buflen != (size_t) -1) { // exists, and is addressable (see readfile2)
FILE *const fp = FOPEN(fconv(catbuff, sizeof(catbuff), fil), "rb");

View File

@@ -676,7 +676,7 @@ int cache_selftests(httrackp *opt, const char *dir) {
char base[HTS_URLMAXSIZE];
strcpybuff(base, dir);
if (base[0] != '\0' && base[strlen(base) - 1] != '/') {
if (base[0] != '\0' && hts_lastchar(base) != '/') {
strcatbuff(base, "/");
}
StringCopy(opt->path_log, base);
@@ -856,7 +856,7 @@ static void golden_setup(httrackp *opt, const char *dir) {
char base[HTS_URLMAXSIZE];
strcpybuff(base, dir);
if (base[0] != '\0' && base[strlen(base) - 1] != '/') {
if (base[0] != '\0' && hts_lastchar(base) != '/') {
strcatbuff(base, "/");
}
StringCopy(opt->path_log, base);
@@ -1195,6 +1195,12 @@ int cache_legacy_refused_selftest(httrackp *opt, const char *dir) {
/* --- read-side corruption injection --------------------------------------- */
/* 100 'A's: a placeholder header line long enough to be overwritten by a
forged, over-long X-StatusMessage of the same byte length. */
#define CORRUPT_LONG_ETAG \
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" \
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
/* canary read back intact after each corruption; victim gets the byte surgery
*/
#define CORRUPT_ADR "corrupt.example.com"
@@ -1219,6 +1225,24 @@ static void corrupt_build(httrackp *opt) {
selftest_close(&cache);
}
/* Like corrupt_build, but the victim carries a 100-char Etag placeholder. */
static void corrupt_build_longetag(httrackp *opt) {
cache_back cache;
memset(corrupt_body_a, 'a', sizeof(corrupt_body_a) - 1);
memset(corrupt_body_b, 'b', sizeof(corrupt_body_b) - 1);
remove(reconcile_st_path(opt, "hts-cache/new.zip"));
remove(reconcile_st_path(opt, "hts-cache/old.zip"));
selftest_open_for_write(&cache, opt);
store_entry(opt, &cache, CORRUPT_ADR, "/canary.html", "canary.html", 200,
"OK", "text/html", "utf-8", "", "", "", "", corrupt_body_a,
strlen(corrupt_body_a));
store_entry(opt, &cache, CORRUPT_ADR, "/victim.html", "victim.html", 200,
"OK", "text/html", "utf-8", "", CORRUPT_LONG_ETAG, "", "",
corrupt_body_b, strlen(corrupt_body_b));
selftest_close(&cache);
}
/* Like corrupt_build, but the victim carries a 20-char Etag whose header line
is later overwritten with a forged oversized X-Size (same byte length). */
static void corrupt_build_etag(httrackp *opt) {
@@ -1403,6 +1427,48 @@ static int corrupt_expect_disk_header(httrackp *opt, LLint wantsize,
return fail;
}
/* An over-long field from a foreign cache must clip, not abort: the entry
still reads, clipped to capacity, and the canary survives. */
static int corrupt_expect_victim_clipped(httrackp *opt, size_t wantmsg,
size_t wantlastmod, const char *what) {
cache_back cache;
htsblk v, c;
char BIGSTK lv[HTS_URLMAXSIZE * 2];
char BIGSTK lc[HTS_URLMAXSIZE * 2];
int fail = 0;
selftest_open_for_read(&cache, opt);
lv[0] = lc[0] = '\0';
v = cache_readex(opt, &cache, CORRUPT_ADR, "/victim.html", "", lv, NULL, 1);
if (v.statuscode != 200) {
fprintf(stderr, "%s: %s: status %d, expected 200\n", selftest_tag, what,
v.statuscode);
fail++;
}
if (wantmsg != (size_t) -1 && strlen(v.msg) != wantmsg) {
fprintf(stderr, "%s: %s: msg len %u, expected %u\n", selftest_tag, what,
(unsigned) strlen(v.msg), (unsigned) wantmsg);
fail++;
}
if (wantlastmod != (size_t) -1 && strlen(v.lastmodified) != wantlastmod) {
fprintf(stderr, "%s: %s: lastmodified len %u, expected %u\n", selftest_tag,
what, (unsigned) strlen(v.lastmodified), (unsigned) wantlastmod);
fail++;
}
c = cache_readex(opt, &cache, CORRUPT_ADR, "/canary.html", "", lc, NULL, 1);
if (c.statuscode != 200) {
fprintf(stderr, "%s: %s: canary tainted (status %d)\n", selftest_tag, what,
c.statuscode);
fail++;
}
if (v.adr != NULL)
freet(v.adr);
if (c.adr != NULL)
freet(c.adr);
selftest_close(&cache);
return fail;
}
/* One zip corruption case: build, patch, then check victim+canary in-session.
*/
static int corrupt_case_zip(httrackp *opt, const char *pat, const char *rep,
@@ -1439,6 +1505,31 @@ int cache_corruption_selftest(httrackp *opt, const char *dir) {
failures += corrupt_expect_victim(opt, "Cache Read Error : Read Data",
"garbled deflate stream");
/* A corrupt cache can hold a field wider than ours. Clipping keeps the
entry; aborting would take the crawl down. Overwrite the placeholder Etag
line in place, same byte length, so the zip offsets stay intact. */
corrupt_build_longetag(opt);
corrupt_patch(opt, "Etag: " CORRUPT_LONG_ETAG, 106,
"X-StatusMessage: " /* 17 + 89 = 106 */
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
1, 1);
failures +=
corrupt_expect_victim_clipped(opt, sizeof(((htsblk *) 0)->msg) - 1,
(size_t) -1, "over-long X-StatusMessage");
/* lastmodified[64] is narrower than msg[80]: one hardcoded clip length
cannot satisfy both. */
corrupt_build_longetag(opt);
corrupt_patch(opt, "Etag: " CORRUPT_LONG_ETAG, 106,
"Last-Modified: " /* 15 + 91 = 106 */
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
1, 1);
failures += corrupt_expect_victim_clipped(
opt, (size_t) -1, sizeof(((htsblk *) 0)->lastmodified) - 1,
"over-long Last-Modified");
/* An X-Size above INT_MAX is positive as int64 (slips a bare sign check) but
truncates negative in the (int) cast the malloc uses: a wraparound alloc.
cache_add asserts size fits an int, so such a value only reaches the reader

679
src/htschanges.c Normal file
View File

@@ -0,0 +1,679 @@
/* ------------------------------------------------------------ */
/*
HTTrack Website Copier, Offline Browser for Windows and Unix
Copyright (C) 2026 Xavier Roche and other contributors
SPDX-License-Identifier: GPL-3.0-or-later
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Ethical use: we kindly ask that you NOT use this software to harvest email
addresses or to collect any other private information about people. Doing so
would dishonor our work and waste the many hours we have spent on it.
Please visit our Website: http://www.httrack.com
*/
/* ------------------------------------------------------------ */
/* File: htschanges.c subroutines: */
/* --changes: what this crawl changed vs. the previous */
/* mirror (hts-changes.json) */
/* Author: Xavier Roche */
/* ------------------------------------------------------------ */
#define HTS_INTERNAL_BYTECODE
#include "htschanges.h"
#include "htscache.h"
#include "htscharset.h"
#include "htscore.h"
#include "htslib.h"
#include "htsmd5.h"
#include "htssafe.h"
#include "htsthread.h"
#include "htstools.h"
#include "coucal/coucal.h"
#include "md5.h"
#include <stdlib.h>
#include <string.h>
#include <time.h>
#define DIGEST_SIZE 16
typedef struct hts_changes hts_changes;
/* One mirrored file. The mirror-relative path is the key, not the URL: a
redirect and its target can share a save name, and counting that file twice
would put it in two buckets. */
typedef struct {
char *url; /* absolute URL, "" for an engine-generated file */
char *file; /* mirror-relative path, as listed in new.lst */
hts_boolean rewritten; /* the crawl wrote over the local copy */
hts_boolean not_updated; /* transfer signal: the server reported no change */
hts_boolean existed; /* the file was part of the previous mirror */
hts_boolean on_disk; /* a copy was on disk when the crawl first saw it */
hts_boolean listed_prev; /* the previous mirror's index listed this file */
hts_boolean has_prev; /* prev_digest holds the previous payload's digest */
hts_boolean has_new; /* new_digest was taken from the payload, not disk */
unsigned char prev_digest[DIGEST_SIZE];
unsigned char new_digest[DIGEST_SIZE];
LLint prev_size; /* -1 when unknown */
LLint size; /* size after the crawl, -1 when unknown */
hts_change_bucket bucket;
} changes_entry;
struct hts_changes {
coucal index; /* mirror-relative path -> entry slot + 1 */
changes_entry *entries;
size_t count;
size_t capacity;
hts_boolean has_index; /* the run keeps a mirror index (the cache is on) */
hts_boolean old_index; /* the previous mirror's index was read */
hts_boolean overflow; /* an allocation failed; the report is partial */
hts_boolean closed; /* reported: late notifies are dropped, not recorded */
};
/* ------------------------------------------------------------ */
/* JSON */
/* ------------------------------------------------------------ */
/* Length of the UTF-8 sequence led by c, or 0 if c cannot lead one. */
static size_t utf8_lead_length(unsigned char c) {
if (c >= 0xc2 && c <= 0xdf)
return 2;
if (c >= 0xe0 && c <= 0xef)
return 3;
if (c >= 0xf0 && c <= 0xf4)
return 4;
return 0;
}
void hts_changes_json_string(String *out, const char *s) {
const unsigned char *p = (const unsigned char *) s;
StringAddchar(*out, '"');
while (*p != '\0') {
const unsigned char c = *p;
if (c == '"' || c == '\\') {
StringAddchar(*out, '\\');
StringAddchar(*out, (char) c);
p++;
} else if (c < 0x20 || c == 0x7f) {
char esc[8];
snprintf(esc, sizeof(esc), "\\u%04x", (unsigned) c);
StringCat(*out, esc);
p++;
} else if (c < 0x80) {
StringAddchar(*out, (char) c);
p++;
} else {
/* Non-ASCII: emit it verbatim only if it is a well-formed sequence, so
a legacy-charset URL cannot produce unparseable JSON. */
const size_t len = utf8_lead_length(c);
if (len != 0 && strnlen((const char *) p, len) == len &&
hts_isStringUTF8((const char *) p, len)) {
StringMemcat(*out, (const char *) p, len);
p += len;
} else {
StringCat(*out, "\\ufffd");
p++;
}
}
}
StringAddchar(*out, '"');
}
/* ------------------------------------------------------------ */
/* Accumulator */
/* ------------------------------------------------------------ */
static hts_changes *changes_new(void) {
hts_changes *changes = calloct(1, sizeof(*changes));
if (changes == NULL)
return NULL;
changes->index = coucal_new(0);
if (changes->index == NULL) {
freet(changes);
return NULL;
}
return changes;
}
static void changes_free(hts_changes **pchanges) {
hts_changes *changes = *pchanges;
if (changes == NULL)
return;
if (changes->entries != NULL) {
size_t i;
for (i = 0; i < changes->count; i++) {
freet(changes->entries[i].url);
freet(changes->entries[i].file);
}
freet(changes->entries);
}
if (changes->index != NULL)
coucal_delete(&changes->index);
freet(changes);
*pchanges = NULL;
}
/* Slot of `file`, or -1 when it has not been recorded. */
static intptr_t changes_find(const hts_changes *changes, const char *file) {
intptr_t slot = 0;
return coucal_read(changes->index, file, &slot) ? slot - 1 : -1;
}
/* Slot of `file`, appending a fresh entry when it is new; -1 if allocation
failed, which flags the report partial. */
static intptr_t changes_slot(hts_changes *changes, const char *file,
hts_boolean *is_new) {
intptr_t slot = 0;
*is_new = HTS_FALSE;
if (coucal_read(changes->index, file, &slot))
return slot - 1;
if (changes->count == changes->capacity) {
const size_t capacity = changes->capacity != 0 ? changes->capacity * 2 : 64;
changes_entry *const entries =
realloct(changes->entries, capacity * sizeof(*entries));
if (entries == NULL) {
changes->overflow = HTS_TRUE;
return -1;
}
changes->entries = entries;
changes->capacity = capacity;
}
slot = (intptr_t) changes->count;
memset(&changes->entries[slot], 0, sizeof(changes->entries[slot]));
changes->entries[slot].file = strdupt(file);
changes->entries[slot].prev_size = -1;
changes->entries[slot].size = -1;
if (changes->entries[slot].file == NULL) {
changes->overflow = HTS_TRUE;
return -1;
}
changes->count++;
coucal_write(changes->index, file, slot + 1);
*is_new = HTS_TRUE;
return slot;
}
/* MD5 of the file at `path`; not a security boundary. Never call it holding
changes_mutex: hashing a large file would stall every other connection.
HTS_FALSE if it cannot be read. */
static hts_boolean digest_file(const char *path,
unsigned char digest[DIGEST_SIZE]) {
const int endian = 1;
struct MD5Context ctx;
char BIGSTK buffer[32768];
FILE *fp = FOPEN(path, "rb");
size_t nread;
if (fp == NULL)
return HTS_FALSE;
MD5Init(&ctx, *((const char *) &endian));
while ((nread = fread(buffer, 1, sizeof(buffer), fp)) > 0)
MD5Update(&ctx, (const unsigned char *) buffer, (unsigned int) nread);
if (ferror(fp) != 0) {
fclose(fp);
return HTS_FALSE;
}
fclose(fp);
MD5Final(digest, &ctx);
return HTS_TRUE;
}
static void digest_mem(const char *buffer, size_t len,
unsigned char digest[DIGEST_SIZE]) {
domd5mem(buffer, len, (char *) digest, 0);
}
hts_change_bucket hts_changes_classify(hts_boolean rewritten,
hts_boolean existed,
hts_boolean not_updated,
hts_boolean have_digests,
hts_boolean digests_equal) {
if (!rewritten)
return HTS_CHANGE_UNCHANGED; /* the crawl left the copy alone */
if (!existed)
return HTS_CHANGE_NEW;
if (have_digests)
return digests_equal ? HTS_CHANGE_UNCHANGED : HTS_CHANGE_CHANGED;
/* No digest to compare: a server with no validators answers 200 with the
same bytes, so this signal alone over-reports. */
return not_updated ? HTS_CHANGE_UNCHANGED : HTS_CHANGE_CHANGED;
}
/* ------------------------------------------------------------ */
/* Engine hooks */
/* ------------------------------------------------------------ */
/* FTP transfers reach file_notify() from a thread the crawl never joins, so
every entry point below, report and teardown included, takes this lock. */
static htsmutex changes_mutex = HTSMUTEX_INIT;
/* The live accumulator, created on first use; NULL when --changes is off or
the report is already written. Call under the lock. */
static hts_changes *changes_get(httrackp *opt) {
hts_changes *changes = (hts_changes *) opt->changes_state;
if (!opt->changes)
return NULL;
if (changes == NULL) {
changes = changes_new();
opt->changes_state = changes;
}
return changes != NULL && !changes->closed ? changes : NULL;
}
void hts_changes_notify(httrackp *opt, const char *adr, const char *fil,
const char *save, hts_boolean rewritten,
hts_boolean not_updated) {
hts_changes *changes;
char BIGSTK file[HTS_URLMAXSIZE * 2];
char BIGSTK url[HTS_URLMAXSIZE * 4 + 8]; /* holds adr + fil + a scheme */
unsigned char prev_digest[DIGEST_SIZE];
hts_boolean has_prev = HTS_FALSE;
hts_boolean is_new = HTS_FALSE;
intptr_t slot = -1;
LLint prev_size;
if (!opt->changes)
return;
/* Engine-generated scaffolding (the top index) carries no URL and is not a
mirrored resource. */
if (save == NULL || !strnotempty(save) || adr == NULL || fil == NULL ||
(!strnotempty(adr) && !strnotempty(fil)))
return;
hts_savename_listed(StringBuff(opt->path_html), save, file, sizeof(file));
hts_mutexlock(&changes_mutex);
changes = changes_get(opt);
if (changes != NULL) {
slot = changes_slot(changes, file, &is_new);
if (slot >= 0 && !is_new) {
/* Retry, or a second call site for the same file: the pre-run state was
already sampled and the copy on disk may no longer be it. */
changes->entries[slot].rewritten =
changes->entries[slot].rewritten || rewritten;
}
}
hts_mutexrelease(&changes_mutex);
if (slot < 0 || !is_new)
return;
url[0] = '\0';
if (!link_has_authority(adr))
strlcatbuff(url, "http://", sizeof(url));
strlcatbuff(url, adr, sizeof(url));
strlcatbuff(url, fil, sizeof(url));
/* Unlocked: hashing a large file would stall every other connection. Hash
it only when it is about to be overwritten, the last moment it exists. */
prev_size = fsize_utf8(save);
if (prev_size >= 0 && rewritten)
has_prev = digest_file(save, prev_digest);
hts_mutexlock(&changes_mutex);
changes = changes_get(opt);
/* The slot index is stable, entries being append-only; the array is not. */
if (changes != NULL && (size_t) slot < changes->count) {
changes_entry *const entry = &changes->entries[slot];
entry->url = strdupt(url);
if (entry->url == NULL)
changes->overflow = HTS_TRUE;
entry->rewritten = entry->rewritten || rewritten;
entry->not_updated = not_updated;
entry->prev_size = prev_size;
entry->on_disk = prev_size >= 0;
/* hts_changes_html() compares payloads, and its digests win. */
if (has_prev && !entry->has_new) {
entry->has_prev = HTS_TRUE;
memcpy(entry->prev_digest, prev_digest, DIGEST_SIZE);
}
}
hts_mutexrelease(&changes_mutex);
}
void hts_changes_html(httrackp *opt, cache_back *cache, const htsblk *r,
const char *adr, const char *fil, const char *save) {
hts_changes *changes;
char BIGSTK file[HTS_URLMAXSIZE * 2];
char BIGSTK location[HTS_URLMAXSIZE * 2];
unsigned char new_digest[DIGEST_SIZE];
unsigned char prev_digest[DIGEST_SIZE];
hts_boolean has_prev = HTS_FALSE;
intptr_t slot;
htsblk prev;
/* Ahead of the digest and the cache read: off must cost nothing. */
if (!opt->changes)
return;
if (r->adr == NULL || r->size < 0 || save == NULL || !strnotempty(save))
return;
/* On disk this is the payload plus rewritten links and a footer dated by the
crawl, so it differs every run; compare payloads, the previous one being
the body the cache kept. */
digest_mem(r->adr, (size_t) r->size, new_digest);
prev = cache_read_ro(opt, cache, adr, fil, "", location);
if (HTTP_IS_OK(prev.statuscode) && prev.adr != NULL && prev.size >= 0) {
digest_mem(prev.adr, (size_t) prev.size, prev_digest);
has_prev = HTS_TRUE;
}
freet(prev.adr);
/* Only now take the lock and look the entry up: cache_read_ro() can itself
reach file_notify(), which would move the entries array. */
hts_mutexlock(&changes_mutex);
changes = changes_get(opt);
/* file_notify() records the entry; this call only refines it. */
if (changes != NULL) {
hts_savename_listed(StringBuff(opt->path_html), save, file, sizeof(file));
slot = changes_find(changes, file);
if (slot >= 0) {
changes_entry *const entry = &changes->entries[slot];
entry->has_new = HTS_TRUE;
memcpy(entry->new_digest, new_digest, DIGEST_SIZE);
entry->has_prev = has_prev;
if (has_prev)
memcpy(entry->prev_digest, prev_digest, DIGEST_SIZE);
}
}
hts_mutexrelease(&changes_mutex);
}
void hts_changes_dropped(httrackp *opt, const char *file, hts_boolean kept) {
hts_changes *changes;
hts_boolean is_new;
intptr_t slot;
if (!opt->changes || file == NULL || !strnotempty(file))
return;
hts_mutexlock(&changes_mutex);
changes = changes_get(opt);
if (changes != NULL) {
slot = changes_slot(changes, file, &is_new);
if (slot >= 0 && is_new) {
changes->entries[slot].url = strdupt("");
changes->entries[slot].listed_prev = HTS_TRUE;
/* A kept file leaves rewritten clear, which resolves to unchanged. */
if (!kept)
changes->entries[slot].bucket = HTS_CHANGE_GONE;
}
}
hts_mutexrelease(&changes_mutex);
}
void hts_changes_previous(httrackp *opt, const char *file) {
hts_changes *changes;
intptr_t slot;
if (!opt->changes || file == NULL)
return;
hts_mutexlock(&changes_mutex);
changes = changes_get(opt);
if (changes != NULL) {
changes->has_index = HTS_TRUE;
changes->old_index = HTS_TRUE;
slot = changes_find(changes, file);
if (slot >= 0)
changes->entries[slot].listed_prev = HTS_TRUE;
}
hts_mutexrelease(&changes_mutex);
}
void hts_changes_indexed(httrackp *opt) {
hts_changes *changes;
if (!opt->changes)
return;
hts_mutexlock(&changes_mutex);
changes = changes_get(opt);
if (changes != NULL)
changes->has_index = HTS_TRUE;
hts_mutexrelease(&changes_mutex);
}
/* ------------------------------------------------------------ */
/* Report */
/* ------------------------------------------------------------ */
/* Assign each entry its final bucket from the bytes now on disk. */
static void changes_resolve(hts_changes *changes, httrackp *opt) {
char catbuff[CATBUFF_SIZE];
size_t i;
for (i = 0; i < changes->count; i++) {
changes_entry *const entry = &changes->entries[i];
const char *path;
unsigned char digest[DIGEST_SIZE];
hts_boolean have_digests = HTS_FALSE;
hts_boolean digests_equal = HTS_FALSE;
if (entry->bucket == HTS_CHANGE_GONE)
continue;
/* The previous mirror's index is the authority on what was there before:
a partial left by this crawl's own failed attempt is on disk but was
never part of the previous mirror. Without an index, fall back to what
the first notify saw on disk. */
entry->existed = changes->old_index ? entry->listed_prev : entry->on_disk;
path = fconcat(catbuff, sizeof(catbuff), StringBuff(opt->path_html),
entry->file);
entry->size = fsize_utf8(path);
if (entry->rewritten && entry->existed) {
/* The size shortcut only holds when both digests describe the file on
disk; has_new means they are payload digests, and a parsed page's
rendered size moves with its links and footer. */
if (!entry->has_new && entry->size >= 0 && entry->prev_size >= 0 &&
entry->size != entry->prev_size) {
have_digests = HTS_TRUE; /* different lengths: no need to hash */
digests_equal = HTS_FALSE;
} else if (entry->has_prev &&
(entry->has_new
? (memcpy(digest, entry->new_digest, DIGEST_SIZE), 1)
: digest_file(path, digest))) {
have_digests = HTS_TRUE;
digests_equal = memcmp(digest, entry->prev_digest, DIGEST_SIZE) == 0
? HTS_TRUE
: HTS_FALSE;
}
}
entry->bucket =
hts_changes_classify(entry->rewritten, entry->existed,
entry->not_updated, have_digests, digests_equal);
}
}
static const char *const bucket_names[HTS_CHANGE_BUCKETS] = {
"new", "changed", "unchanged", "gone"};
/* Serialize the report. Call under changes_mutex. */
static void changes_serialize(httrackp *opt, String *out) {
hts_changes *const changes = (hts_changes *) opt->changes_state;
size_t counts[HTS_CHANGE_BUCKETS];
char date[32];
char scratch[64];
int bucket;
size_t i;
StringClear(*out);
if (changes == NULL)
return;
changes_resolve(changes, opt);
memset(counts, 0, sizeof(counts));
for (i = 0; i < changes->count; i++)
counts[changes->entries[i].bucket]++;
hts_now_iso8601(date);
StringCat(*out, "{\n \"schema\": ");
snprintf(scratch, sizeof(scratch), "%d", HTS_CHANGES_SCHEMA);
StringCat(*out, scratch);
StringCat(*out, ",\n \"generator\": ");
hts_changes_json_string(out, "HTTrack Website Copier/" HTTRACK_VERSION);
StringCat(*out, ",\n \"date\": ");
hts_changes_json_string(out, date);
StringCat(*out, ",\n \"first_crawl\": ");
StringCat(*out, !changes->has_index
? "null"
: (changes->old_index ? "false" : "true"));
StringCat(*out, ",\n \"partial\": ");
StringCat(*out, changes->overflow ? "true" : "false");
StringCat(*out, ",\n \"purged\": ");
StringCat(*out, opt->delete_old ? "true" : "false");
StringCat(*out, ",\n \"counts\": {");
for (bucket = 0; bucket < HTS_CHANGE_BUCKETS; bucket++) {
StringCat(*out, bucket != 0 ? ", " : " ");
hts_changes_json_string(out, bucket_names[bucket]);
snprintf(scratch, sizeof(scratch), ": %d", (int) counts[bucket]);
StringCat(*out, scratch);
}
StringCat(*out, " }");
for (bucket = 0; bucket < HTS_CHANGE_BUCKETS; bucket++) {
hts_boolean first = HTS_TRUE;
StringCat(*out, ",\n ");
hts_changes_json_string(out, bucket_names[bucket]);
StringCat(*out, ": [");
for (i = 0; i < changes->count; i++) {
const changes_entry *const entry = &changes->entries[i];
if (entry->bucket != bucket)
continue;
StringCat(*out, first ? "\n { \"url\": " : ",\n { \"url\": ");
first = HTS_FALSE;
hts_changes_json_string(out, entry->url != NULL ? entry->url : "");
StringCat(*out, ", \"file\": ");
hts_changes_json_string(out, entry->file);
if (entry->size >= 0) {
snprintf(scratch, sizeof(scratch), ", \"size\": " LLintP,
(LLint) entry->size);
StringCat(*out, scratch);
}
if (bucket == HTS_CHANGE_CHANGED && entry->prev_size >= 0) {
snprintf(scratch, sizeof(scratch), ", \"previous_size\": " LLintP,
(LLint) entry->prev_size);
StringCat(*out, scratch);
}
StringCat(*out, " }");
}
StringCat(*out, first ? "]" : "\n ]");
}
StringCat(*out, "\n}\n");
}
void hts_changes_report(httrackp *opt, String *out) {
hts_mutexlock(&changes_mutex);
changes_serialize(opt, out);
hts_mutexrelease(&changes_mutex);
}
void hts_changes_close_opt(httrackp *opt) {
char catbuff[CATBUFF_SIZE];
const char *path;
String report = STRING_EMPTY;
size_t counts[HTS_CHANGE_BUCKETS];
size_t total;
hts_boolean has_index, old_index;
FILE *fp;
hts_mutexlock(&changes_mutex);
{
/* changes_get(), not the raw field: a crawl that mirrored nothing still
owes the user a report, and a stale one from the previous run must not
survive on disk as if it described this one. */
hts_changes *const changes = changes_get(opt);
size_t i;
if (changes == NULL) {
hts_mutexrelease(&changes_mutex);
return;
}
changes_serialize(opt, &report);
memset(counts, 0, sizeof(counts));
for (i = 0; i < changes->count; i++)
counts[changes->entries[i].bucket]++;
total = changes->count;
has_index = changes->has_index;
old_index = changes->old_index;
/* Sticky: whatever the crawl's stragglers do next is not in this report. */
changes->closed = HTS_TRUE;
}
hts_mutexrelease(&changes_mutex);
path = fconcat(catbuff, sizeof(catbuff), StringBuff(opt->path_log),
HTS_CHANGES_FILE);
fp = FOPEN(path, "wb");
if (fp != NULL) {
const size_t len = StringLength(report);
if (len != 0 && fwrite(StringBuff(report), 1, len, fp) != len)
hts_log_print(opt, LOG_ERROR | LOG_ERRNO,
"Unable to write the change report %s", path);
fclose(fp);
} else {
hts_log_print(opt, LOG_ERROR | LOG_ERRNO,
"Unable to create the change report %s", path);
}
if (!has_index) {
hts_log_print(opt, LOG_NOTICE,
"Change report: no mirror index (the cache is off), %d files "
"mirrored, deletions not detected (%s)",
(int) total, HTS_CHANGES_FILE);
} else if (!old_index) {
hts_log_print(opt, LOG_NOTICE,
"Change report: first crawl, %d files mirrored, nothing to "
"compare against (%s)",
(int) total, HTS_CHANGES_FILE);
} else {
hts_log_print(opt, LOG_NOTICE,
"Change report: %d new, %d changed, %d unchanged, %d gone "
"(%s)",
(int) counts[HTS_CHANGE_NEW],
(int) counts[HTS_CHANGE_CHANGED],
(int) counts[HTS_CHANGE_UNCHANGED],
(int) counts[HTS_CHANGE_GONE], HTS_CHANGES_FILE);
}
StringFree(report);
}
void hts_changes_free_opt(httrackp *opt) {
hts_changes *changes;
hts_mutexlock(&changes_mutex);
changes = (hts_changes *) opt->changes_state;
changes_free(&changes);
opt->changes_state = NULL;
hts_mutexrelease(&changes_mutex);
}

125
src/htschanges.h Normal file
View File

@@ -0,0 +1,125 @@
/* ------------------------------------------------------------ */
/*
HTTrack Website Copier, Offline Browser for Windows and Unix
Copyright (C) 2026 Xavier Roche and other contributors
SPDX-License-Identifier: GPL-3.0-or-later
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Ethical use: we kindly ask that you NOT use this software to harvest email
addresses or to collect any other private information about people. Doing so
would dishonor our work and waste the many hours we have spent on it.
Please visit our Website: http://www.httrack.com
*/
/* ------------------------------------------------------------ */
/* HTTrack change report (--changes). Internal, not installed.
Accumulates what the crawl did to each mirrored file, compares the bytes
against the copy the previous run left behind, and writes
hts-changes.json next to the log. */
/* ------------------------------------------------------------ */
#ifndef HTS_CHANGES_DEFH
#define HTS_CHANGES_DEFH
#include "htsopt.h"
#include "htsstrings.h"
#ifndef HTS_DEF_FWSTRUCT_cache_back
#define HTS_DEF_FWSTRUCT_cache_back
typedef struct cache_back cache_back;
#endif
#ifdef __cplusplus
extern "C" {
#endif
/* Report file name, written under the project's log directory. */
#define HTS_CHANGES_FILE "hts-changes.json"
/* Schema version carried by the report; bump on an incompatible change. */
#define HTS_CHANGES_SCHEMA 1
/* Which side of the comparison a mirrored file ended up on. */
typedef enum {
HTS_CHANGE_NEW = 0, /* no local copy before this run */
HTS_CHANGE_CHANGED, /* rewritten, and the bytes differ */
HTS_CHANGE_UNCHANGED, /* the bytes are those of the previous mirror */
HTS_CHANGE_GONE, /* in the previous mirror, absent from this one */
HTS_CHANGE_BUCKETS
} hts_change_bucket;
/* Record what this crawl is doing to the local file `save` (absolute path;
adr/fil form its URL, either may be empty for an engine-generated file).
`rewritten` means the copy on disk is being written over, `not_updated` is
the 200-versus-304 signal, used only when no digest can be taken. Only the
first call for a file samples the previous copy, so a retried or
twice-notified resource is counted once. No-op unless --changes is on. */
void hts_changes_notify(httrackp *opt, const char *adr, const char *fil,
const char *save, hts_boolean rewritten,
hts_boolean not_updated);
/* Refine the entry for a parsed HTML file: its local copy carries rewritten
links and a footer dated by the crawl, so it differs every run. Compares the
payload `r` holds against the body the previous run left in the cache. */
void hts_changes_html(httrackp *opt, cache_back *cache, const htsblk *r,
const char *adr, const char *fil, const char *save);
/* Record `file` (mirror-relative, as listed in new.lst) as listed by the
previous mirror's index and absent from this run's. `kept` means its local
copy survives the run because the crawl tried and failed to replace it: a
failed transfer is not a deletion, so it is reported unchanged, not gone. */
void hts_changes_dropped(httrackp *opt, const char *file, hts_boolean kept);
/* Record that the previous mirror's index listed `file`. That index, not the
file's presence on disk, decides what counts as already mirrored. */
void hts_changes_previous(httrackp *opt, const char *file);
/* Record that this run keeps a mirror index. Without one (the cache is off)
deletions and first_crawl are undecidable, and the report says so. */
void hts_changes_indexed(httrackp *opt);
/* Resolve every entry against the bytes now on disk and serialize the report
into `out` (replaced). Exposed for the self-tests. */
void hts_changes_report(httrackp *opt, String *out);
/* Write the report and log a one-line summary. Idempotent, and seals the
accumulator: a late notify is dropped rather than starting a second one. */
void hts_changes_close_opt(httrackp *opt);
/* Drop the accumulator, for a run that never reached its end and to start the
next one clean. Null-safe and idempotent. */
void hts_changes_free_opt(httrackp *opt);
/* Bucket for one entry, from what was observed. No filesystem access:
`have_digests` says both sides could be hashed, `digests_equal` compares
them, and `not_updated` is the fallback signal when they could not. */
hts_change_bucket hts_changes_classify(hts_boolean rewritten,
hts_boolean existed,
hts_boolean not_updated,
hts_boolean have_digests,
hts_boolean digests_equal);
/* Append `s` to `out` as a quoted JSON string. Byte sequences that are not
valid UTF-8 become U+FFFD, so a mirror carrying legacy-charset URLs still
produces parseable JSON. */
void hts_changes_json_string(String *out, const char *s);
#ifdef __cplusplus
}
#endif
#endif

95
src/htscmdline.c Normal file
View File

@@ -0,0 +1,95 @@
/* ------------------------------------------------------------ */
/*
HTTrack Website Copier, Offline Browser for Windows and Unix
Copyright (C) 1998 Xavier Roche and other contributors
SPDX-License-Identifier: GPL-3.0-or-later
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Ethical use: we kindly ask that you NOT use this software to harvest email
addresses or to collect any other private information about people. Doing so
would dishonor our work and waste the many hours we have spent on it.
Please visit our Website: http://www.httrack.com
*/
/* ------------------------------------------------------------ */
/* File: command line splitter, shared by the engine and */
/* htsserver */
/* Author: Xavier Roche */
/* ------------------------------------------------------------ */
#include "htscmdline.h"
#include "htssafe.h"
#include <limits.h>
#include <stdint.h>
char **hts_split_cmdline(char *cmd, int *nargs) {
size_t nsep = 0;
size_t capacity;
size_t r;
size_t w;
int argc = 0;
hts_boolean quoted = HTS_FALSE;
char **argv;
*nargs = 0;
/* fold the other separators, so counting them sizes the vector exactly */
for (r = 0; cmd[r] != '\0'; r++) {
if (cmd[r] == '\t' || cmd[r] == '\r' || cmd[r] == '\n') {
cmd[r] = ' ';
}
if (cmd[r] == ' ') {
nsep++;
}
}
/* at most one argument per separator, plus the leading one and the NULL */
if (nsep > (size_t) INT_MAX - 1 || nsep > SIZE_MAX / sizeof(char *) - 2) {
return NULL;
}
capacity = nsep + 2;
argv = (char **) malloct(capacity * sizeof(char *));
if (argv == NULL) {
return NULL;
}
argv[argc++] = cmd;
for (r = 0, w = 0; cmd[r] != '\0';) {
if (quoted && cmd[r] == '\\' &&
(cmd[r + 1] == '\\' || cmd[r + 1] == '\"')) {
r++;
cmd[w++] = cmd[r++];
} else if (cmd[r] == '\"') {
quoted = !quoted;
cmd[w++] = cmd[r++];
} else if (cmd[r] == ' ' && !quoted) {
cmd[w++] = '\0';
assertf((size_t) argc < capacity - 1); /* the last slot holds the NULL */
argv[argc++] = cmd + w;
r++;
} else {
cmd[w++] = cmd[r++];
}
}
cmd[w] = '\0';
argv[argc] = NULL; /* callers may rely on argv[argc] == NULL */
*nargs = argc;
return argv;
}

45
src/htscmdline.h Normal file
View File

@@ -0,0 +1,45 @@
/* ------------------------------------------------------------ */
/*
HTTrack Website Copier, Offline Browser for Windows and Unix
Copyright (C) 1998 Xavier Roche and other contributors
SPDX-License-Identifier: GPL-3.0-or-later
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Ethical use: we kindly ask that you NOT use this software to harvest email
addresses or to collect any other private information about people. Doing so
would dishonor our work and waste the many hours we have spent on it.
Please visit our Website: http://www.httrack.com
*/
/* ------------------------------------------------------------ */
/* File: command line splitter, shared by the engine and */
/* htsserver */
/* Author: Xavier Roche */
/* ------------------------------------------------------------ */
#ifndef HTSCMDLINE_DEFH
#define HTSCMDLINE_DEFH
#include "htsglobal.h"
/* Split "cmd" in place into a NULL-terminated argv vector of *nargs entries,
argv[0] being the program name and quotes left for the engine to strip.
Returns a malloct'ed vector of pointers into cmd (freet the vector, never its
entries), or NULL when it cannot be sized or allocated. */
char **hts_split_cmdline(char *cmd, int *nargs);
#endif

View File

@@ -72,7 +72,7 @@ hts_codec hts_codec_parse(const char *encoding) {
return HTS_CODEC_IDENTITY;
if (strfield2(encoding, "gzip") || strfield2(encoding, "x-gzip") ||
strfield2(encoding, "deflate") || strfield2(encoding, "x-deflate"))
return HTS_USEZLIB ? HTS_CODEC_DEFLATE : HTS_CODEC_UNSUPPORTED;
return HTS_CODEC_DEFLATE;
if (strfield2(encoding, "br"))
return HTS_USEBROTLI ? HTS_CODEC_BROTLI : HTS_CODEC_UNSUPPORTED;
if (strfield2(encoding, "zstd"))
@@ -98,16 +98,11 @@ hts_codec hts_codec_parse(const char *encoding) {
const char *hts_acceptencoding(hts_boolean compressible, hts_boolean secure) {
if (!compressible)
return "identity";
#if HTS_USEZLIB
/* br and zstd over TLS only, as browsers do: a cleartext intermediary that
rewrites a coding it can not read would corrupt the mirror. */
if (secure)
return "gzip, deflate" HTS_AE_BROTLI HTS_AE_ZSTD ", identity;q=0.9";
return "gzip, deflate, identity;q=0.9";
#else
(void) secure;
return "identity";
#endif
}
hts_boolean hts_codec_is_archive_ext(hts_codec codec, const char *ext) {
@@ -300,11 +295,7 @@ int hts_codec_unpack(hts_codec codec, const char *filename,
return -1;
switch (codec) {
case HTS_CODEC_DEFLATE:
#if HTS_USEZLIB
return hts_zunpack(filename, newfile);
#else
return -1;
#endif
case HTS_CODEC_BROTLI:
case HTS_CODEC_ZSTD:
break;
@@ -348,10 +339,8 @@ size_t hts_codec_head(hts_codec codec, const void *in, size_t in_len, void *out,
if (in == NULL || in_len == 0 || out == NULL || out_len == 0)
return 0;
switch (codec) {
#if HTS_USEZLIB
case HTS_CODEC_DEFLATE:
return hts_zhead(in, in_len, out, out_len);
#endif
#if HTS_USEBROTLI
case HTS_CODEC_BROTLI:
return codec_head_brotli(in, in_len, out, out_len);

View File

@@ -39,7 +39,10 @@ Please visit our Website: http://www.httrack.com
/* File defs */
#include "htscore.h"
#include "htssitemap.h"
#include "htswarc.h"
#include "htschanges.h"
#include "htssinglefile.h"
/* specific definitions */
#include "htsbase.h"
@@ -447,13 +450,22 @@ void hts_finish_makeindex(httrackp *opt, int *makeindex_done,
const char *fil) {
if (!*makeindex_done) {
if (*makeindex_fp) {
char BIGSTK tempo[1024];
/* sized off link_escaped below: at the old flat 1024 a long first link
produced a redirect to a clipped URL */
char BIGSTK tempo[HTS_URLMAXSIZE * 2 + 64];
if (makeindex_links == 1) {
char BIGSTK link_escaped[HTS_URLMAXSIZE * 2];
escape_uri_utf(makeindex_firstlink, link_escaped, sizeof(link_escaped));
snprintf(tempo, sizeof(tempo),
"<meta HTTP-EQUIV=\"Refresh\" CONTENT=\"0; URL=%s\">" CRLF,
link_escaped);
/* no redirect beats one pointing at a clipped URL */
if (!sprintfbuff(
tempo,
"<meta HTTP-EQUIV=\"Refresh\" CONTENT=\"0; URL=%s\">" CRLF,
link_escaped)) {
hts_log_print(opt, LOG_WARNING,
"index redirect omitted: first link too long (%s)",
makeindex_firstlink);
tempo[0] = '\0';
}
} else
tempo[0] = '\0';
hts_template_format(*makeindex_fp, template_footer,
@@ -479,6 +491,7 @@ void hts_finish_html_file(httrackp *opt, cache_back *cache, htsblk *r,
const char *adr, const char *fil, const char *save) {
{
file_notify(opt, adr, fil, save, 1, 1, r->notmodified);
hts_changes_html(opt, cache, r, adr, fil, save);
*fp = filecreate(&opt->state.strc, save);
if (*fp) {
if (ht_len > 0 && fwrite(ht_buff, 1, ht_len, *fp) != ht_len) {
@@ -682,6 +695,9 @@ int httpmirror(char *url1, httrackp * opt) {
// hash table
opt->hash = &hash;
// a change report left by a previous crawl on this opt is not this one's
hts_changes_free_opt(opt);
// initialize link heap
hts_record_init(opt);
@@ -757,7 +773,7 @@ int httpmirror(char *url1, httrackp * opt) {
// sauter les + sans rien après..
if (strnotempty(tempo)) {
if ((plus == 0) && (type == 1)) { // implicite: *www.edf.fr par exemple
if (tempo[strlen(tempo) - 1] != '*') {
if (hts_lastchar(tempo) != '*') {
strcatbuff(tempo, "*"); // ajouter un *
}
}
@@ -934,6 +950,22 @@ int httpmirror(char *url1, httrackp * opt) {
heap_top()->premier = heap_top_index(); // premier lien, objet-père=objet
heap_top()->precedent = heap_top_index(); // lien précédent
/* --sitemap: queue the sitemap probe just after the seeds, so its URLs are
injected before the crawl gets far. */
hts_sitemap_free(opt); /* an earlier mirror may have left a doc list */
if (opt->sitemap || StringNotEmpty(opt->sitemap_url)) {
char BIGSTK first[HTS_URLMAXSIZE * 2];
const char *const eol = strchr(primary, '\n');
const size_t len = eol != NULL ? (size_t) (eol - primary) : 0;
first[0] = '\0';
if (len > 0 && len < sizeof(first)) {
memcpy(first, primary, len);
first[len] = '\0';
}
hts_sitemap_seed(opt, first);
}
// Initialiser cache
{
opt->state._hts_in_html_parsing = 4;
@@ -1578,11 +1610,21 @@ int httpmirror(char *url1, httrackp * opt) {
stre.maketrack_fp = maketrack_fp;
/* Parse */
if (hts_mirror_check_moved(&str, &stre) != 0) {
XH_uninit;
return -1;
}
{
const int nlinks = opt->lien_tot;
if (hts_mirror_check_moved(&str, &stre) != 0) {
XH_uninit;
return -1;
}
/* A redirect re-queues the target as a fresh link; without carrying
the marking over, a moved sitemap is fetched and then ignored. */
if (opt->sitemap_state != NULL && opt->lien_tot > nlinks &&
hts_sitemap_pending(opt, urladr(), urlfil())) {
hts_sitemap_redirect(opt, urladr(), urlfil(), heap_top()->adr,
heap_top()->fil);
}
}
}
} // if !error
@@ -1600,6 +1642,29 @@ int httpmirror(char *url1, httrackp * opt) {
/* Load file and decode if necessary, after redirect check. */
LOAD_IN_MEMORY_IF_NECESSARY();
/* Sitemap document: turn its <loc> URLs into top-level seeds. They go
through htsAddLink, so the wizard's filters and scope rules decide, and
this link's max depth leaves them the full budget. */
if (opt->sitemap_state != NULL &&
hts_sitemap_pending(opt, urladr(), urlfil())) {
htsmoduleStruct BIGSTK smstr;
int smptr = ptr;
memset(&smstr, 0, sizeof(smstr));
smstr.opt = opt;
smstr.sback = sback;
smstr.cache = &cache;
smstr.hashptr = hashptr;
smstr.numero_passe = numero_passe;
smstr.ptr_ = &smptr; /* scratch: the ingester retargets the wizard */
smstr.addLink = htsAddLink;
smstr.url_host = urladr();
smstr.url_file = urlfil();
smstr.mime = r.contenttype;
hts_sitemap_ingest(opt, &smstr, urladr(), urlfil(), r.adr,
r.adr != NULL && r.size > 0 ? (size_t) r.size : 0);
}
// ------------------------------------------------------
// ok, fichier chargé localement
// ------------------------------------------------------
@@ -1754,62 +1819,11 @@ int httpmirror(char *url1, httrackp * opt) {
// -- -- --
// sauver fichier
/* En cas d'erreur, vérifier que fichier d'erreur existe */
if (strnotempty(savename()) == 0) { // chemin de sauvegarde existant
if (strcmp(urlfil(), "/robots.txt") == 0) { // pas robots.txt
if (store_errpage) { // c'est une page d'erreur
int create_html_warning = 0;
int create_gif_warning = 0;
switch (ishtml(opt, urlfil())) { /* pas fichier html */
case 0: /* non html */
{
char buff[256];
guess_httptype_sized(opt, buff, sizeof(buff), urlfil());
if (strcmp(buff, "image/gif") == 0)
create_gif_warning = 1;
}
break;
case 1: /* html */
if (!r.adr) {
}
break;
default: /* don't know.. */
break;
}
/* Créer message d'erreur ? */
if (create_html_warning) {
char *adr =
(char *) malloct(strlen(HTS_DATA_ERROR_HTML) + 1100);
hts_log_print(opt, LOG_INFO, "Creating HTML warning file (%s)",
r.msg);
if (adr) {
if (r.adr) {
freet(r.adr);
r.adr = NULL;
}
sprintf(adr, HTS_DATA_ERROR_HTML, r.msg);
r.adr = adr;
}
} else if (create_gif_warning) {
char *adr = (char *) malloct(HTS_DATA_UNKNOWN_GIF_LEN);
hts_log_print(opt, LOG_INFO, "Creating GIF dummy file (%s)",
r.msg);
if (r.adr) {
freet(r.adr);
r.adr = NULL;
}
memcpy(adr, HTS_DATA_UNKNOWN_GIF, HTS_DATA_UNKNOWN_GIF_LEN);
r.adr = adr;
}
}
}
}
if (strnotempty(savename()) == 0) { // pas de chemin de sauvegarde
if (strcmp(urlfil(), "/robots.txt") == 0) { // robots.txt
char BIGSTK sitemaps[8192];
sitemaps[0] = '\0';
if (r.adr) {
char BIGSTK infobuff[8192];
#ifdef IGNORE_RESTRICTIVE_ROBOTS
@@ -1821,7 +1835,8 @@ int httpmirror(char *url1, httrackp * opt) {
#endif
robots_parse(&robots, urladr(), r.adr, r.size, infobuff,
sizeof(infobuff), keep_root);
sizeof(infobuff), keep_root, sitemaps,
sizeof(sitemaps));
if (strnotempty(infobuff)) {
hts_log_print(opt, LOG_INFO,
"Note: robots.txt forbidden links for %s are: %s",
@@ -1831,6 +1846,10 @@ int httpmirror(char *url1, httrackp * opt) {
urladr(), infobuff);
}
}
/* After robots_parse, so the rules this very body carries already
gate the sitemap fetch. Runs even on a failed probe, which is
what falls back to the well-known location. */
hts_sitemap_robots(opt, urladr(), sitemaps);
}
} else if (r.is_write) { // déja sauvé sur disque
/*
@@ -1907,10 +1926,9 @@ int httpmirror(char *url1, httrackp * opt) {
}
// ATTENTION C'EST ICI QU'ON SAUVE LE FICHIER!!
// An empty body must not overwrite the file when the transfer failed
// (statuscode <= 0, e.g. an -M hard-stop): it would truncate a good
// copy to 0 (#77 follow-up).
if (r.adr != NULL || (r.size == 0 && r.statuscode > 0)) {
// A failed transfer has no body: r.adr holds debris from the aborted
// read, which would destroy the copy being re-fetched (#748).
if (r.statuscode > 0 && (r.adr != NULL || r.size == 0)) {
file_notify(opt, urladr(), urlfil(), savename(), 1, 1, r.notmodified);
if (filesave(opt, r.adr, (int) r.size, savename(), urladr(), urlfil()) !=
0) {
@@ -2080,9 +2098,12 @@ int httpmirror(char *url1, httrackp * opt) {
if (cache.lst) {
fclose(cache.lst);
cache.lst = opt->state.strc.lst = NULL;
if (opt->delete_old) {
/* old.lst minus new.lst is the set of files the previous mirror had and
this one does not. --changes reports it; only --purge-old acts on it. */
if (opt->delete_old || opt->changes) {
FILE *old_lst, *new_lst;
hts_changes_indexed(opt);
//
opt->state._hts_in_html_parsing = 3;
//
@@ -2108,30 +2129,47 @@ int httpmirror(char *url1, httrackp * opt) {
int purge = 0;
while(!feof(old_lst)) {
linput(old_lst, line, 1000);
if (!strstr(adr, line)) { // not found in the new list?
char BIGSTK file[HTS_URLMAXSIZE * 2];
char BIGSTK file[HTS_URLMAXSIZE * 2];
strcpybuff(file, StringBuff(opt->path_html));
strcatbuff(file, line + 1);
file[strlen(file) - 1] = '\0';
if (fexist_utf8(file)) { // toujours sur disque: virer
hts_log_print(opt, LOG_INFO, "Purging %s", file);
UNLINK(file);
purge = 1;
linput(old_lst, line, 1000);
if (!strnotempty(line))
continue;
strcpybuff(file, StringBuff(opt->path_html));
strcatbuff(file, line + 1);
/* strip filenote()'s ']', absent when linput() truncated the
line */
hts_striplastchar(file, ']');
hts_changes_previous(opt, file + StringLength(opt->path_html));
if (!strstr(adr, line)) { // not found in the new list?
if (fexist_utf8(file)) { // still on disk
/* A link this crawl did try but never wrote (a transfer
killed mid-flight) also drops out of new.lst. Unless it
is about to be purged, its previous copy stands and the
file is not gone. */
const hts_boolean kept =
!opt->delete_old &&
hash_read(opt->hash, file, NULL,
HASH_STRUCT_FILENAME) >= 0;
hts_changes_dropped(
opt, file + StringLength(opt->path_html), kept);
if (opt->delete_old) {
hts_log_print(opt, LOG_INFO, "Purging %s", file);
UNLINK(file);
purge = 1;
}
}
}
}
{
if (opt->delete_old) { // emptied directories go with the files
fseek(old_lst, 0, SEEK_SET);
while(!feof(old_lst)) {
linput(old_lst, line, 1000);
while(strnotempty(line) && (line[strlen(line) - 1] != '/')
&& (line[strlen(line) - 1] != '\\')) {
line[strlen(line) - 1] = '\0';
while (strnotempty(line) && (hts_lastchar(line) != '/') &&
(hts_lastchar(line) != '\\')) {
hts_choplastchar(line);
}
if (strnotempty(line))
line[strlen(line) - 1] = '\0';
hts_choplastchar(line);
if (strnotempty(line))
if (!strstr(adr, line)) { // non trouvé?
char BIGSTK file[HTS_URLMAXSIZE * 2];
@@ -2144,20 +2182,19 @@ int httpmirror(char *url1, httrackp * opt) {
if (opt->log) {
hts_log_print(opt, LOG_INFO, "Purging directory %s/",
file);
while(strnotempty(file)
&& (file[strlen(file) - 1] != '/')
&& (file[strlen(file) - 1] != '\\')) {
file[strlen(file) - 1] = '\0';
while (strnotempty(file) &&
(hts_lastchar(file) != '/') &&
(hts_lastchar(file) != '\\')) {
hts_choplastchar(file);
}
if (strnotempty(file))
file[strlen(file) - 1] = '\0';
hts_choplastchar(file);
}
}
}
}
}
//
if (!purge) {
if (opt->delete_old && !purge) {
hts_log_print(opt, LOG_INFO, "No files purged");
}
}
@@ -2173,6 +2210,10 @@ int httpmirror(char *url1, httrackp * opt) {
}
// fin purge!
/* --single-file: inline each page's assets now the tree is final, after the
purge has deleted whatever this run dropped. */
singlefile_process_mirror(opt);
// Indexation
if (opt->kindex)
index_finish(StringBuff(opt->path_html), opt->kindex);
@@ -2247,6 +2288,8 @@ int httpmirror(char *url1, httrackp * opt) {
// ending
usercommand(opt, 0, NULL, NULL, NULL, NULL);
warc_close_opt(opt);
hts_changes_close_opt(opt);
hts_sitemap_free(opt);
// désallocation mémoire & buffers
XH_uninit;
@@ -2595,7 +2638,11 @@ HTSEXT_API int structcheck(const char *path) {
if (!S_ISDIR(st.st_mode)) {
#if HTS_REMOVE_ANNOYING_INDEX
if (S_ISREG(st.st_mode)) { /* Regular file in place ; move it and create directory */
sprintf(tmpbuf, "%s.txt", file);
/* bounded here, not by the path-length guard far above */
if (!sprintfbuff(tmpbuf, "%s.txt", file)) {
errno = ENAMETOOLONG;
return -1;
}
if (rename(file, tmpbuf) != 0) { /* Can't rename regular file */
return -1;
}
@@ -2703,7 +2750,11 @@ HTSEXT_API int structcheck_utf8(const char *path) {
if (!S_ISDIR(st.st_mode)) {
#if HTS_REMOVE_ANNOYING_INDEX
if (S_ISREG(st.st_mode)) { /* Regular file in place ; move it and create directory */
sprintf(tmpbuf, "%s.txt", file);
/* bounded here, not by the path-length guard far above */
if (!sprintfbuff(tmpbuf, "%s.txt", file)) {
errno = ENAMETOOLONG;
return -1;
}
if (RENAME(file, tmpbuf) != 0) { /* Can't rename regular file */
return -1;
}
@@ -2872,6 +2923,17 @@ int filecreateempty(filenote_strc * strc, const char *filename) {
return 0;
}
void hts_savename_listed(const char *root, const char *s, char *dest,
size_t destsize) {
char catbuff[CATBUFF_SIZE];
strlcpybuff(dest, fslash(catbuff, sizeof(catbuff), s), destsize);
if (strnotempty(root) && strncmp(fslash(catbuff, sizeof(catbuff), root), dest,
strlen(root)) == 0) {
strlcpybuff(dest, s + strlen(root), destsize);
}
}
// noter fichier
int filenote(filenote_strc * strc, const char *s, filecreate_params * params) {
// gestion du fichier liste liste
@@ -2881,15 +2943,8 @@ int filenote(filenote_strc * strc, const char *s, filecreate_params * params) {
return 0;
} else if (strc->lst) {
char BIGSTK savelst[HTS_URLMAXSIZE * 2];
char catbuff[CATBUFF_SIZE];
strcpybuff(savelst, fslash(catbuff, sizeof(catbuff), s));
// couper chemin?
if (strnotempty(strc->path)) {
if (strncmp(fslash(catbuff, sizeof(catbuff), strc->path), savelst, strlen(strc->path)) == 0) { // couper
strcpybuff(savelst, s + strlen(strc->path));
}
}
hts_savename_listed(strc->path, s, savelst, sizeof(savelst));
fprintf(strc->lst, "[%s]" LF, savelst);
fflush(strc->lst);
}
@@ -2899,6 +2954,9 @@ int filenote(filenote_strc * strc, const char *s, filecreate_params * params) {
/* Note: utf-8 */
void file_notify(httrackp * opt, const char *adr, const char *fil,
const char *save, int create, int modify, int not_updated) {
hts_changes_notify(opt, adr, fil, save,
(create || modify) ? HTS_TRUE : HTS_FALSE,
not_updated ? HTS_TRUE : HTS_FALSE);
RUN_CALLBACK6(opt, filesave2, adr, fil, save, create, modify, not_updated);
}
@@ -3633,6 +3691,15 @@ HTSEXT_API int copy_htsopt(const httrackp * from, httrackp * to) {
to->warc_max_size = from->warc_max_size;
to->warc_cdx = from->warc_cdx;
to->warc_wacz = from->warc_wacz;
to->changes = from->changes;
to->single_file = from->single_file;
if (from->single_file_max_size > 0)
to->single_file_max_size = from->single_file_max_size;
if (from->sitemap)
to->sitemap = from->sitemap;
if (StringNotEmpty(from->sitemap_url))
StringCopyS(to->sitemap_url, from->sitemap_url);
if (from->pause_max_ms > 0) {
to->pause_min_ms = from->pause_min_ms;
@@ -3707,11 +3774,12 @@ int htsAddLink(htsmoduleStruct * str, char *link) {
strcpybuff(codebase, heap(ptr)->fil);
else
strcpybuff(codebase, heap(heap(ptr)->precedent)->fil);
a = codebase + strlen(codebase) - 1;
// empty codebase has no last char; codebase-1 would underflow
a = codebase[0] != '\0' ? codebase + strlen(codebase) - 1 : codebase;
while((*a) && (*a != '/') && (a > codebase))
a--;
if (*a == '/')
*(a + 1) = '\0'; // couper
*(a + 1) = '\0'; // cut
} else { // couper http:// éventuel
if (strfield(codebase, "http://")) {
char BIGSTK tempo[HTS_URLMAXSIZE * 2];

View File

@@ -354,6 +354,12 @@ int filecreateempty(filenote_strc * strct, const char *filename);
int filenote(filenote_strc * strct, const char *s, filecreate_params * params);
/* Copy into dest (destsize bytes) the form under which the local path s is
listed in new.lst: forward slashes, with the mirror `root` stripped when s
sits under it. Also the change report's key, so the two must not drift. */
void hts_savename_listed(const char *root, const char *s, char *dest,
size_t destsize);
void file_notify(httrackp * opt, const char *adr, const char *fil,
const char *save, int create, int modify, int wasupdated);
@@ -370,8 +376,8 @@ void hts_finish_makeindex(httrackp *opt, int *makeindex_done,
const char *template_footer, const char *adr,
const char *fil);
// Flush ht_buff[0..ht_len] to save on disk (skip if MD5 unchanged); *fp
// closed+NULLed on write. Precondition: ht_len>0.
// Flush ht_buff[0..ht_len] to save on disk; *fp closed+NULLed on write.
// Precondition: ht_len>0.
void hts_finish_html_file(httrackp *opt, cache_back *cache, htsblk *r,
FILE **fp, const char *ht_buff, size_t ht_len,
const char *adr, const char *fil, const char *save);
@@ -409,6 +415,8 @@ char *readfile2(const char *fil, LLint * size);
char *readfile_utf8(const char *fil);
char *readfile2_utf8(const char *fil, LLint *size);
char *readfile_or(const char *fil, const char *defaultdata);
/* Backing (download-slot) scheduler. Operate on the back[] ring (struct_back).

View File

@@ -41,6 +41,7 @@ Please visit our Website: http://www.httrack.com
#include "htsdefines.h"
#include "htsalias.h"
#include "htswarc.h"
#include "htschanges.h"
#include "htsbauth.h"
#include "htswrap.h"
#include "htsmodules.h"
@@ -356,7 +357,7 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
char BIGSTK tempo[HTS_CDLMAXSIZE];
strcpybuff(tempo, argv[na] + 1);
if (tempo[0] == '\0' || tempo[strlen(tempo) - 1] != '"') {
if (hts_lastchar(tempo) != '"') {
char BIGSTK s[HTS_CDLMAXSIZE];
sprintf(s, "Missing quote in %s", argv[na]);
@@ -364,7 +365,7 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
htsmain_free();
return -1;
}
tempo[strlen(tempo) - 1] = '\0';
hts_choplastchar(tempo);
/* tempo is argv[na] minus its surrounding quotes, so it fits in place
*/
strlcpybuff(argv[na], tempo, strlen(argv[na]) + 1);
@@ -862,7 +863,7 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
char BIGSTK tempo[HTS_CDLMAXSIZE + 256];
strcpybuff(tempo, argv[na] + 1);
if (tempo[0] == '\0' || tempo[strlen(tempo) - 1] != '"') {
if (hts_lastchar(tempo) != '"') {
char s[HTS_CDLMAXSIZE + 256];
sprintf(s, "Missing quote in %s", argv[na]);
@@ -870,7 +871,7 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
htsmain_free();
return -1;
}
tempo[strlen(tempo) - 1] = '\0';
hts_choplastchar(tempo);
/* tempo is argv[na] minus its surrounding quotes, so it fits in place
*/
strlcpybuff(argv[na], tempo, strlen(argv[na]) + 1);
@@ -1747,6 +1748,13 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
StringCopy(opt->cookies_file, argv[na]);
}
break;
case 'd': // --changes: report what this crawl changed
opt->changes = HTS_TRUE;
if (*(com + 1) == '0') {
opt->changes = HTS_FALSE;
com++;
}
break;
case 'r': // warc / warc-file: write an ISO-28500 WARC archive
if (*(com + 1) == 'f') { // --warc-file NAME: explicit basename
com++;
@@ -1795,6 +1803,56 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
StringCopy(opt->warc_file, WARC_AUTONAME);
}
break;
case 'Z': // single-file: inline each page's assets as data: URIs
if (*(com + 1) == 's') { // --single-file-max-size N
com++;
if ((na + 1 >= argc) || (argv[na + 1][0] == '-')) {
HTS_PANIC_PRINTF(
"Option single-file-max-size needs a blank "
"space and a size");
htsmain_free();
return -1;
}
na++;
{ // reject non-numeric/negative/overflow; keep the default
char *end;
LLint v;
errno = 0;
v = strtoll(argv[na], &end, 10);
if (isdigit((unsigned char) argv[na][0]) && *end == '\0' &&
errno != ERANGE && v > 0)
opt->single_file_max_size = v;
}
opt->single_file = HTS_TRUE;
} else {
opt->single_file = HTS_TRUE;
if (*(com + 1) == '0') {
opt->single_file = HTS_FALSE;
com++;
}
}
break;
case 'm': // sitemap / sitemap-url: seed the crawl from sitemaps
if (*(com + 1) == 'u') { // --sitemap-url URL: explicit sitemap
com++;
if ((na + 1 >= argc) || (argv[na + 1][0] == '-')) {
HTS_PANIC_PRINTF(
"Option sitemap-url needs a blank space and a URL");
htsmain_free();
return -1;
}
na++;
if (strlen(argv[na]) >= HTS_URLMAXSIZE) {
HTS_PANIC_PRINTF("Sitemap URL too long");
htsmain_free();
return -1;
}
StringCopy(opt->sitemap_url, argv[na]);
} else { // --sitemap: robots.txt probe, then /sitemap.xml
opt->sitemap = HTS_TRUE;
}
break;
case 'Y': // why: explain the filter verdict for a URL, no crawl
if ((na + 1 >= argc) || (argv[na + 1][0] == '-')) {
HTS_PANIC_PRINTF("Option why needs a blank space and a URL");
@@ -2666,10 +2724,7 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
char *a;
strcpybuff(rpath, StringBuff(opt->path_html));
if (rpath[0]) {
if (rpath[strlen(rpath) - 1] == '/')
rpath[strlen(rpath) - 1] = '\0';
}
hts_striplastchar(rpath, '/');
a = strrchr(rpath, '/');
if (a) {
*a = '\0';

View File

@@ -251,7 +251,7 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
// folding a nonsense port into 1..65535 fetches one the link never named;
// an empty "host:" just means the default (#614)
if (a[1] != '\0' && !hts_parse_url_port(a + 1, &port)) {
snprintf(back->r.msg, sizeof(back->r.msg), "Invalid port: %s", a + 1);
htsblk_failf(&back->r, "Invalid port: %s", a + 1);
back->r.statuscode = STATUSCODE_INVALID; // permanent, unlike a DNS miss
_HALT_FTP return 0;
}
@@ -262,8 +262,7 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
// récupérer adresse résolue
strcpybuff(back->info, "host name");
if (hts_dns_resolve2(opt, _adr, &server, &error) == NULL) {
snprintf(back->r.msg, sizeof(back->r.msg),
"Unable to get server's address: %s", error);
htsblk_failf(&back->r, "Unable to get server's address: %s", error);
back->r.statuscode = STATUSCODE_NON_FATAL;
_HALT_FTP return 0;
}
@@ -332,18 +331,15 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
}
} else {
snprintf(back->r.msg, sizeof(back->r.msg), "Bad password: %s",
linejmp(line));
htsblk_failf(&back->r, "Bad password: %s", linejmp(line));
back->r.statuscode = STATUSCODE_INVALID;
}
} else {
snprintf(back->r.msg, sizeof(back->r.msg), "Bad user name: %s",
linejmp(line));
htsblk_failf(&back->r, "Bad user name: %s", linejmp(line));
back->r.statuscode = STATUSCODE_INVALID;
}
} else {
snprintf(back->r.msg, sizeof(back->r.msg), "Connection refused: %s",
linejmp(line));
htsblk_failf(&back->r, "Connection refused: %s", linejmp(line));
back->r.statuscode = STATUSCODE_INVALID;
}
@@ -410,8 +406,7 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
}
// -- fin analyse de l'adresse IP et du port --
} else {
snprintf(back->r.msg, sizeof(back->r.msg), "PASV incorrect: %s",
linejmp(line));
htsblk_failf(&back->r, "PASV incorrect: %s", linejmp(line));
back->r.statuscode = STATUSCODE_INVALID;
} // sinon on est prêts
} else {
@@ -442,13 +437,11 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
}
}
} else {
snprintf(back->r.msg, sizeof(back->r.msg), "EPSV incorrect: %s",
linejmp(line));
htsblk_failf(&back->r, "EPSV incorrect: %s", linejmp(line));
back->r.statuscode = STATUSCODE_INVALID;
}
} else {
snprintf(back->r.msg, sizeof(back->r.msg), "PASV/EPSV error: %s",
linejmp(line));
htsblk_failf(&back->r, "PASV/EPSV error: %s", linejmp(line));
back->r.statuscode = STATUSCODE_INVALID;
} // sinon on est prêts
}
@@ -490,16 +483,19 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
back->r.totalsize = size;
}
}
// REST?
if (fexist(back->url_sav) && (transfer_list == 0)) {
/* Only over a copy back_add() judged partial: on --update every
mirrored file exists, and resuming a complete one splices the
old body into the new (#798). */
if (back->range_req_size > 0 && (transfer_list == 0)) {
strcpybuff(back->info, "rest");
snprintf(line, sizeof(line), "REST " LLintP, (LLint) fsize(back->url_sav));
snprintf(line, sizeof(line), "REST " LLintP,
(LLint) back->range_req_size);
send_line(soc_ctl, line);
get_ftp_line(soc_ctl, line, sizeof(line), timeout);
_CHECK_HALT_FTP;
if ((line[0] == '3') || (line[0] == '2')) { // ok
rest_understood = 1;
} // sinon tant pis
} // else never mind
}
} // sinon tant pis
}
@@ -554,8 +550,8 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
deletesoc(soc_dat);
soc_dat = INVALID_SOCKET;
//
snprintf(back->r.msg, sizeof(back->r.msg),
"RETR command error: %s", linejmp(line));
htsblk_failf(&back->r, "RETR command error: %s",
linejmp(line));
back->r.statuscode = STATUSCODE_INVALID;
} // sinon on est prêts
} else {
@@ -573,13 +569,12 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
back->r.statuscode = STATUSCODE_INVALID;
} // sinon on est prêts
} else {
snprintf(back->r.msg, sizeof(back->r.msg),
"Unable to resolve IP %s: %s", adr_ip, error);
htsblk_failf(&back->r, "Unable to resolve IP %s: %s", adr_ip,
error);
back->r.statuscode = STATUSCODE_INVALID;
} // sinon on est prêts
} else {
snprintf(back->r.msg, sizeof(back->r.msg), "PASV incorrect: %s",
linejmp(line));
htsblk_failf(&back->r, "PASV incorrect: %s", linejmp(line));
back->r.statuscode = STATUSCODE_INVALID;
} // sinon on est prêts
#else
@@ -603,13 +598,11 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
back->r.statuscode = STATUSCODE_INVALID;
}
} else {
snprintf(back->r.msg, sizeof(back->r.msg),
"RETR command error: %s", linejmp(line));
htsblk_failf(&back->r, "RETR command error: %s", linejmp(line));
back->r.statuscode = STATUSCODE_INVALID;
}
} else {
snprintf(back->r.msg, sizeof(back->r.msg), "PORT command error: %s",
linejmp(line));
htsblk_failf(&back->r, "PORT command error: %s", linejmp(line));
back->r.statuscode = STATUSCODE_INVALID;
}
#ifdef _WIN32
@@ -627,13 +620,19 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
// Ok, connexion initiée
//
if (soc_dat != INVALID_SOCKET) {
if (rest_understood) { // REST envoyée et comprise
if (rest_understood) { // REST sent and understood
file_notify(opt, back->url_adr, back->url_fil, back->url_sav, 0, 1,
0);
/* The bytes already on disk count too, or the completeness check
below rejects every resumed transfer (#798). */
back->r.size = back->range_req_size;
back->r.fp = fileappend(&opt->state.strc, back->url_sav);
} else {
file_notify(opt, back->url_adr, back->url_fil, back->url_sav, 1, 1,
0);
/* Every failure exit below would else leave the mirror truncated
(#771); the resume branch appends and needs no backup. */
back_refetch_backup(opt, back);
back->r.fp = filecreate(&opt->state.strc, back->url_sav);
}
strcpybuff(back->info, "receiving");
@@ -652,8 +651,7 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
len = 0; // fin
break;
case 0:
snprintf(back->r.msg, sizeof(back->r.msg), "Time out (%d)",
timeout);
htsblk_failf(&back->r, "Time out (%d)", timeout);
back->r.statuscode = STATUSCODE_INVALID;
len = 0; // fin
break;
@@ -716,8 +714,7 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
strcpybuff(back->r.msg, "OK");
back->r.statuscode = HTTP_OK;
} else {
snprintf(back->r.msg, sizeof(back->r.msg), "RETR incorrect: %s",
linejmp(line));
htsblk_failf(&back->r, "RETR incorrect: %s", linejmp(line));
back->r.statuscode = STATUSCODE_INVALID;
}
} else {

View File

@@ -72,7 +72,8 @@ Please visit our Website: http://www.httrack.com
HTS_UNUSED: suppress unused-symbol warnings. HTS_STATIC: an unused-safe
static. HTS_PRINTF_FUN(fmt, arg): mark a printf-like function so the
compiler type-checks the format string at argument index fmt against the
varargs starting at arg. */
varargs starting at arg. HTS_CHECK_RESULT: the return value carries the only
error signal, so dropping it is a bug; a (void) cast does not silence it. */
#ifndef HTS_UNUSED
#ifdef __GNUC__
#define HTS_UNUSED __attribute__((unused))
@@ -80,10 +81,13 @@ Please visit our Website: http://www.httrack.com
#define HTS_STATIC static __attribute__((unused))
#define HTS_PRINTF_FUN(fmt, arg) __attribute__((format(printf, fmt, arg)))
#define HTS_CHECK_RESULT __attribute__((warn_unused_result))
#else
#define HTS_UNUSED
#define HTS_STATIC static
#define HTS_PRINTF_FUN(fmt, arg)
#define HTS_CHECK_RESULT
#endif
#endif
@@ -134,10 +138,11 @@ Please visit our Website: http://www.httrack.com
#define HTS_DOSNAME 0
#endif
// utiliser zlib?
// zlib is mandatory: the cache is a zip and minizip calls it regardless
#ifndef HTS_USEZLIB
// autoload
#define HTS_USEZLIB 1
#elif !HTS_USEZLIB
#error HTS_USEZLIB=0 is not a supported configuration
#endif
// brotli and zstd content codings; off unless the build opted in (configure,

View File

@@ -83,9 +83,8 @@ void infomsg(const char *msg) {
/* try the flag as-is, then strip a trailing N as the numeric-arg
placeholder (cN -> c); this order keeps -%N from becoming -% */
p = optreal_find(cmd);
if (p < 0 && (int) strlen(cmd) > 2 &&
cmd[strlen(cmd) - 1] == 'N') {
cmd[strlen(cmd) - 1] = '\0';
if (p < 0 && (int) strlen(cmd) > 2 && hts_lastchar(cmd) == 'N') {
hts_striplastchar(cmd, 'N');
p = optreal_find(cmd);
}
if (p >= 0) {
@@ -124,7 +123,9 @@ typedef struct help_wizard_buffers {
char stropt[2048]; // options
char stropt2[2048]; // options longues
char strwild[2048]; // wildcards
char cmd[4096];
/* holds all four of the above plus separators: at 4096 a long answer set
clipped the filters off the command line */
char cmd[HTS_URLMAXSIZE * 2 + 3 * 2048 + 4];
char str[256];
char *argv[256];
} help_wizard_buffers;
@@ -209,7 +210,7 @@ void help_wizard(httrackp * opt) {
strcatbuff(str, "/websites/");
}
if (strnotempty(str))
if ((str[strlen(str) - 1] != '/') && (str[strlen(str) - 1] != '\\'))
if ((hts_lastchar(str) != '/') && (hts_lastchar(str) != '\\'))
strcatbuff(str, "/");
strcatbuff(stropt2, "-O \"");
strcatbuff(stropt2, str);
@@ -308,7 +309,14 @@ void help_wizard(httrackp * opt) {
printf("\n");
if (strlen(stropt) == 1)
stropt[0] = '\0'; // aucune
snprintf(cmd, sizeof(cmd), "%s %s %s %s", urls, stropt, stropt2, strwild);
/* the tail is the filter list, and cmd is split into the argv handed to
hts_main() below: a clipped line would silently widen the crawl */
if (!sprintfbuff(cmd, "%s %s %s %s", urls, stropt, stropt2, strwild)) {
printf("* command line too long (%d bytes max)\n",
(int) sizeof(cmd) - 1);
freet(buffers);
return;
}
printf("---> Wizard command line: httrack %s\n\n", cmd);
printf("Ready to launch the mirror? (Y/n) :");
fflush(stdout);
@@ -424,7 +432,8 @@ void help_catchurl(const char *dest_path) {
}
// former URL!
{
char BIGSTK finalurl[HTS_URLMAXSIZE * 2];
/* url and dest are each HTS_URLMAXSIZE*2, plus the POSTTOK marker */
char BIGSTK finalurl[HTS_URLMAXSIZE * 4 + 32];
inplace_escape_check_url(dest, sizeof(dest));
snprintf(finalurl, sizeof(finalurl), "%s" POSTTOK "file:%s", url, dest);
@@ -516,6 +525,11 @@ void help(const char *app, int more) {
(" %L <file> add all URL located in this text file (one URL per line)");
infomsg
(" %S <file> add all scan rules located in this text file (one scan rule per line)");
infomsg(" %m seed the crawl from the site's sitemap (robots.txt Sitemap:, "
"then /sitemap.xml); --sitemap-url URL names one explicitly. A "
"sitemap you name, or one the site declares, is fetched even under "
"robots.txt Disallow; only the guessed /sitemap.xml obeys it. The "
"URLs found still pass every filter and scope rule");
infomsg("");
infomsg("Build options:");
infomsg(" NN structure type (0 *original structure, 1+: see below)");
@@ -525,6 +539,13 @@ void help(const char *app, int more) {
infomsg
(" %D cached delayed type check, don't wait for remote type during updates, to speedup them (%D0 wait, * %D1 don't wait)");
infomsg(" %M generate a RFC MIME-encapsulated full-archive (.mht)");
infomsg(" %Z after the mirror, rewrite each saved page with its "
"stylesheets, scripts, images and fonts inlined as data: URIs, so "
"any page opens by double-click anywhere (links between pages stay "
"relative; audio and video stay links); --single-file-max-size N "
"caps each asset (default 10485760 bytes). %M is the better "
"container where a Chromium-family browser is a given: one archive, "
"no base64 tax on text, a shared asset stored once");
infomsg(" %t keep the original file extension, don't rewrite it from the "
"MIME type (%t0 rewrite)");
infomsg
@@ -537,8 +558,7 @@ void help(const char *app, int more) {
infomsg
(" %q *include query string for local files (useless, for information purpose only) (%q0 don't include)");
infomsg(" %g strip query keys for dedup ([host/pattern=]key1,key2,...)");
infomsg
(" o *generate output html file in case of error (404..) (o0 don't generate)");
infomsg(" o *save the server's error pages (404..) (o0 discard them)");
infomsg(" X *purge old files after update (X0 keep delete)");
infomsg(" %p preserve html files 'as is' (identical to '-K4 -%F \"\"')");
infomsg(" %T links conversion to UTF-8");
@@ -598,6 +618,8 @@ void help(const char *app, int more) {
"output name, --warc-max-size N rotates segments past N bytes, "
"--warc-cdx also writes a sorted CDXJ index, --wacz packages it all "
"as a WACZ file");
infomsg(" %d write hts-changes.json listing what this crawl left new, "
"changed, unchanged and gone compared to the previous mirror");
infomsg(" %n do not re-download locally erased files");
infomsg
(" %v display on screen filenames downloaded (in realtime) - * %v1 short version - %v2 full animation");

View File

@@ -36,7 +36,10 @@ Please visit our Website: http://www.httrack.com
// Fichier librairie .c
#include "htscore.h"
#include "htssitemap.h"
#include "htswarc.h"
#include "htschanges.h"
#include "htssinglefile.h"
/* specific definitions */
#include "htsbase.h"
@@ -708,11 +711,11 @@ T_SOC http_xfopen(httrackp *opt, int mode, int treat, int waitconnect,
#ifdef _WIN32
int last_errno = WSAGetLastError();
sprintf(retour->msg, "Connect error: %s", strerror(last_errno));
htsblk_failf(retour, "Connect error: %s", strerror(last_errno));
#else
int last_errno = errno;
sprintf(retour->msg, "Connect error: %s", strerror(last_errno));
htsblk_failf(retour, "Connect error: %s", strerror(last_errno));
#endif
}
}
@@ -1128,12 +1131,10 @@ int http_sendhead(httrackp * opt, t_cookie * cookie, int mode,
// Compression accepted ?
if (retour->req.http11) {
hts_boolean compressible = HTS_FALSE;
hts_boolean compressible =
(!retour->req.range_used && !retour->req.nocompression);
hts_boolean secure = HTS_FALSE;
#if HTS_USEZLIB
compressible = (!retour->req.range_used && !retour->req.nocompression);
#endif
#if HTS_USEOPENSSL
secure = retour->ssl ? HTS_TRUE : HTS_FALSE;
#endif
@@ -2240,13 +2241,13 @@ T_SOC newhttp_addr(httrackp *opt, const char *_iadr, htsblk *retour, int port,
#ifdef _WIN32
int last_errno = WSAGetLastError();
sprintf(retour->msg, "Unable to create a socket: %s",
strerror(last_errno));
htsblk_failf(retour, "Unable to create a socket: %s",
strerror(last_errno));
#else
int last_errno = errno;
sprintf(retour->msg, "Unable to create a socket: %s",
strerror(last_errno));
htsblk_failf(retour, "Unable to create a socket: %s",
strerror(last_errno));
#endif
}
return INVALID_SOCKET; // erreur création socket impossible
@@ -2312,13 +2313,13 @@ T_SOC newhttp_addr(httrackp *opt, const char *_iadr, htsblk *retour, int port,
#ifdef _WIN32
const int last_errno = WSAGetLastError();
sprintf(retour->msg, "Unable to connect to the server: %s",
strerror(last_errno));
htsblk_failf(retour, "Unable to connect to the server: %s",
strerror(last_errno));
#else
const int last_errno = errno;
sprintf(retour->msg, "Unable to connect to the server: %s",
strerror(last_errno));
htsblk_failf(retour, "Unable to connect to the server: %s",
strerror(last_errno));
#endif
}
/* Close the socket and notify the error!!! */
@@ -2698,6 +2699,15 @@ void time_gmt_rfc822(char *s) {
time_rfc822(s, A);
}
void hts_now_iso8601(char out[32]) {
time_t t = time(NULL);
struct tm tmv;
if (!hts_gmtime(t, &tmv))
memset(&tmv, 0, sizeof(tmv));
strftime(out, 32, "%Y-%m-%dT%H:%M:%SZ", &tmv);
}
// heure actuelle, format rfc (taille buffer 256o)
void time_local_rfc822(char *s) {
time_t tt;
@@ -5059,7 +5069,7 @@ static int hts_dns_resolve_nocache_list(const char *const hostname,
if (!strnotempty(hostname) || max <= 0) {
return 0;
}
if ((hostname[0] == '[') && (hostname[strlen(hostname) - 1] == ']')) {
if ((hostname[0] == '[') && (hts_lastchar(hostname) == ']')) {
size_t size = strlen(hostname);
char *copy = malloct(size + 1);
int count;
@@ -5472,9 +5482,8 @@ void cut_path(char *fullpath, char *path, size_t path_size, char *pname,
size_t pname_size) {
path[0] = pname[0] = '\0';
if (strnotempty(fullpath)) {
if ((fullpath[strlen(fullpath) - 1] == '/')
|| (fullpath[strlen(fullpath) - 1] == '\\'))
fullpath[strlen(fullpath) - 1] = '\0';
if (!hts_striplastchar(fullpath, '/'))
hts_striplastchar(fullpath, '\\');
if (strlen(fullpath) > 1) {
char *a;
@@ -5772,7 +5781,8 @@ HTSEXT_API void hts_log_vprint(httrackp * opt, int type, const char *format, va_
if (hts_log_print_callback != NULL) {
va_list args_copy;
va_copy(args_copy, args);
hts_log_print_callback(opt, type, format, args);
/* the copy, so the vfprintf() below still has an unread list */
hts_log_print_callback(opt, type, format, args_copy);
va_end(args_copy);
}
if (opt != NULL && opt->log != NULL) {
@@ -6010,7 +6020,12 @@ HTSEXT_API httrackp *hts_create_opt(void) {
StringCopy(opt->strip_query, "");
StringCopy(opt->cookies_file, "");
StringCopy(opt->warc_file, "");
StringCopy(opt->sitemap_url, "");
opt->warc_max_size = 0; /* no rotation unless --warc-max-size sets it */
opt->changes = HTS_FALSE;
opt->changes_state = NULL;
opt->single_file = HTS_FALSE;
opt->single_file_max_size = SINGLEFILE_DEFAULT_MAX_SIZE;
StringCopy(opt->why_url, "");
opt->pause_min_ms = 0;
opt->pause_max_ms = 0;
@@ -6163,6 +6178,10 @@ HTSEXT_API void hts_free_opt(httrackp * opt) {
StringFree(opt->cookies_file);
StringFree(opt->why_url);
StringFree(opt->warc_file);
StringFree(opt->sitemap_url);
hts_sitemap_free(opt); /* backstop: httpmirror's early-return paths */
hts_changes_free_opt(opt);
StringFree(opt->path_html);
StringFree(opt->path_html_utf8);
@@ -6620,9 +6639,12 @@ int hts_rename_utf8(const char *oldpath, const char *newpath) {
LPWSTR wnewpath = hts_pathToUCS2(newpath);
if (woldpath != NULL && wnewpath != NULL) {
const int result = _wrename(woldpath, wnewpath);
/* Save errno: callers key off it (#779) and free() may clobber it. */
const int err = errno;
free(woldpath);
free(wnewpath);
errno = err;
return result;
} else {
if (woldpath != NULL)

View File

@@ -157,6 +157,19 @@ struct t_dnscache {
char host_addr[HTS_MAXADDRNUM][HTS_MAXADDRLEN];
};
/* Break t down as UTC into the caller's buffer, HTS_FALSE if that failed.
gmtime()'s static is shared, and both the engine and ProxyTrack convert on
worker threads. */
static HTS_INLINE HTS_UNUSED hts_boolean hts_gmtime(time_t t,
struct tm *tmbuf) {
#ifdef _WIN32
/* Microsoft's gmtime_s takes the destination first, unlike C11 Annex K. */
return gmtime_s(tmbuf, &t) == 0 ? HTS_TRUE : HTS_FALSE;
#else
return gmtime_r(&t, tmbuf) != NULL ? HTS_TRUE : HTS_FALSE;
#endif
}
/* Library internal definictions */
#ifdef HTS_INTERNAL_BYTECODE
@@ -199,6 +212,11 @@ T_SOC newhttp(httrackp * opt, const char *iadr, htsblk * retour, int port,
etc.). */
T_SOC newhttp_addr(httrackp *opt, const char *iadr, htsblk *retour, int port,
int waitconnect, int addr_index, int *addr_count);
/* Clips the formatted failure reason into r->msg, which also round-trips
through the cache as X-StatusMessage. Leaves r->statuscode to the caller. */
#define htsblk_failf(R, ...) \
slprintfbuff_clip((R)->msg, sizeof((R)->msg), __VA_ARGS__)
HTS_INLINE void deletehttp(htsblk * r);
HTS_INLINE int deleteaddr(htsblk * r);
HTS_INLINE void deletesoc(T_SOC soc);
@@ -253,6 +271,10 @@ void sec2str(char *s, TStamp t);
void time_gmt_rfc822(char *s);
void time_local_rfc822(char *s);
/* Current UTC time as "YYYY-MM-DDThh:mm:ssZ". */
void hts_now_iso8601(char out[32]);
struct tm *convert_time_rfc822(struct tm *buffer, const char *s);
int set_filetime(const char *file, struct tm *tm_time);
int set_filetime_rfc822(const char *file, const char *date);

View File

@@ -43,9 +43,7 @@ Please visit our Website: http://www.httrack.com
#include "htsencoding.h"
#include "htssniff.h"
#include "htscodec.h"
#if HTS_USEZLIB
#include "htszlib.h"
#endif
#include <ctype.h>
#include <limits.h>
@@ -439,9 +437,7 @@ int url_savename(lien_adrfilsave *const afs,
strcpybuff(fil_complete_patche, normfil);
// Version avec ou sans /
if (fil_complete_patche[strlen(fil_complete_patche) - 1] == '/')
fil_complete_patche[strlen(fil_complete_patche) - 1] = '\0';
else
if (!hts_striplastchar(fil_complete_patche, '/'))
strcatbuff(fil_complete_patche, "/");
i = hash_read(hash, normadr, fil_complete_patche, HASH_STRUCT_ORIGINAL_ADR_PATH); // recherche table 2 (former->adr+former->fil)
if (i >= 0) {
@@ -517,7 +513,8 @@ int url_savename(lien_adrfilsave *const afs,
&& protocol != PROTOCOL_FTP
) {
// tester type avec requète HEAD si on ne connait pas le type du fichier
if (!((opt->check_type == 1) && (fil[strlen(fil) - 1] == '/'))) // slash doit être html?
if (!((opt->check_type == 1) &&
(hts_lastchar(fil) == '/'))) // slash doit être html?
if (opt->savename_delayed == HTS_SAVENAME_DELAYED_HARD ||
ishtml(opt, fil) < 0) { // unsure whether it's html or a file
// lire dans le cache
@@ -812,7 +809,7 @@ int url_savename(lien_adrfilsave *const afs,
// - - - DEBUT NOMMAGE - - -
// Donner nom par défaut?
if (fil[strlen(fil) - 1] == '/') {
if (hts_lastchar(fil) == '/') {
if (!strfield(adr_complete, "ftp://")
) {
strcatbuff(fil, DEFAULT_HTML); // nommer page par défaut!!
@@ -1286,7 +1283,7 @@ int url_savename(lien_adrfilsave *const afs,
hts_lowcase(afs->save);
if (afs->save[strlen(afs->save) - 1] == '/')
if (hts_lastchar(afs->save) == '/')
strcatbuff(afs->save, DEFAULT_HTML); // nommer page par défaut!!
}

View File

@@ -547,6 +547,23 @@ struct httrackp {
archive. Tail: ABI */
hts_boolean warc_wacz; /**< --wacz: package archive+index+pages as a WACZ zip
(implies --warc + --warc-cdx). Tail: ABI */
hts_boolean changes; /**< --changes: report what this crawl changed against
the previous mirror. Tail: ABI */
void *changes_state; /**< live change-report accumulator (hts_changes*),
engine-owned. Tail: ABI */
hts_boolean single_file; /**< --single-file: once the mirror is done, rewrite
each saved page with its assets inlined as
data: URIs. Tail: ABI */
LLint single_file_max_size; /**< --single-file-max-size: per-asset cap in
bytes; a bigger asset stays a link.
Tail: ABI */
hts_boolean sitemap; /**< --sitemap: probe the start host's robots.txt for
Sitemap: lines, else /sitemap.xml. Tail: ABI */
String sitemap_url; /**< --sitemap-url: sitemap to ingest. Tail: ABI */
/* Live state, not an option: copy_htsopt must leave it alone. It sits here
rather than in htsoptstate because that struct is embedded by value, so
growing it would shift every httrackp field declared after it. */
void *sitemap_state; /**< hts_sitemap_state*, or NULL. Tail: ABI */
};
/* Running statistics for a mirror. */

View File

@@ -1701,7 +1701,8 @@ int htsparse(htsmoduleStruct * str, htsmoduleStructExtended * stre) {
#endif
) // ok pas de problème
url_ok = 1;
else if (tempo[strlen(tempo) - 1] == '/') { // un slash: ok..
else if (hts_lastchar(tempo) ==
'/') { // un slash: ok..
if (inscript) // sinon si pas javascript, méfiance (répertoire style base?)
url_ok = 1;
}
@@ -2005,9 +2006,8 @@ int htsparse(htsmoduleStruct * str, htsmoduleStructExtended * stre) {
if (eadr - html - 1 < HTS_URLMAXSIZE) { // pas trop long?
strncpy(lien, html, eadr - html - 1);
lien[eadr - html - 1] = '\0';
// supprimer les espaces
while((lien[strlen(lien) - 1] == ' ') && (strnotempty(lien)))
lien[strlen(lien) - 1] = '\0';
while (hts_striplastchar(lien, ' ')) {
}
} else
lien[0] = '\0'; // erreur
@@ -2207,7 +2207,7 @@ int htsparse(htsmoduleStruct * str, htsmoduleStructExtended * stre) {
// supposition dangereuse?
// OUI!!
#if HTS_TILDE_SLASH
if (lien[strlen(lien) - 1] != '/') {
if (hts_lastchar(lien) != '/') {
char *a = lien + strlen(lien) - 1;
// éviter aussi index~1.html
@@ -2272,7 +2272,7 @@ int htsparse(htsmoduleStruct * str, htsmoduleStructExtended * stre) {
// Vérifier les codebase=applet (au lieu de applet/)
if (p_type == -2) { // codebase
if (strnotempty(lien)) {
if (lien[strlen(lien) - 1] != '/') { // pas répertoire
if (hts_lastchar(lien) != '/') { // pas répertoire
strcatbuff(lien, "/");
}
}
@@ -2688,9 +2688,11 @@ int htsparse(htsmoduleStruct * str, htsmoduleStructExtended * stre) {
int cat_data_len = 0;
// ajouter lien external
switch ((link_has_authority(afs.af.adr)) ? 1
: ((afs.af.fil[strlen(afs.af.fil) - 1] ==
'/') ? 1 : (ishtml(opt, afs.af.fil)))) {
switch ((link_has_authority(afs.af.adr))
? 1
: ((hts_lastchar(afs.af.fil) == '/')
? 1
: (ishtml(opt, afs.af.fil)))) {
case 1:
case -2: // html ou répertoire
if (opt->getmode & HTS_GETMODE_HTML) {
@@ -2733,7 +2735,7 @@ int htsparse(htsmoduleStruct * str, htsmoduleStructExtended * stre) {
cat_data_len = HTS_DATA_UNKNOWN_HTML_LEN;
}
break;
} // html,gif
} // html,gif
if (patch_it) {
char BIGSTK save[HTS_URLMAXSIZE * 2];

View File

@@ -173,8 +173,8 @@ int http_proxy_tunnel(httrackp *opt, htsblk *retour, const char *adr,
if (sscanf(line, "HTTP/%*d.%*d %d", &code) < 1)
code = 0;
if (code < 200 || code >= 300) {
snprintf(retour->msg, sizeof(retour->msg), "proxy CONNECT refused: %s",
strnotempty(line) ? line : "(no status)");
htsblk_failf(retour, "proxy CONNECT refused: %s",
strnotempty(line) ? line : "(no status)");
return 0;
}

View File

@@ -147,7 +147,8 @@ static void robots_blob_add(char *blob, size_t blobsize, char marker,
void robots_parse(robots_wizard *robots, const char *adr, const char *body,
size_t bodysize, char *info, size_t infosize,
hts_boolean keep_root_disallow) {
hts_boolean keep_root_disallow, char *sitemaps,
size_t sitemapsize) {
size_t bptr = 0;
int record = 0;
char BIGSTK line[1024];
@@ -156,6 +157,8 @@ void robots_parse(robots_wizard *robots, const char *adr, const char *body,
blob[0] = '\0';
if (info != NULL && infosize > 0)
info[0] = '\0';
if (sitemaps != NULL && sitemapsize > 0)
sitemaps[0] = '\0';
#if DEBUG_ROBOTS
printf("robots.txt dump:\n%s\n", body);
#endif
@@ -172,7 +175,19 @@ void robots_parse(robots_wizard *robots, const char *adr, const char *body,
line[llen - 1] = '\0';
llen--;
}
if (strfield(line, "user-agent:")) {
if (sitemaps != NULL && strfield(line, "sitemap:")) {
// group-independent record (RFC 9309): collected whatever the group
char *a = line + 8;
while (is_realspace(*a))
a++;
/* A line at the buffer limit was truncated: a half URL is not one. */
if (strnotempty(a) && strlen(line) < sizeof(line) - 3 &&
strlen(a) + 2 < sitemapsize - strlen(sitemaps)) {
strlcatbuff(sitemaps, a, sitemapsize);
strlcatbuff(sitemaps, "\n", sitemapsize);
}
} else if (strfield(line, "user-agent:")) {
char *a = line + 11;
while (is_realspace(*a))

View File

@@ -56,10 +56,12 @@ int checkrobots(robots_wizard * robots, const char *adr, const char *fil);
void checkrobots_free(robots_wizard * robots);
int checkrobots_set(robots_wizard * robots, const char *adr, const char *data);
/* Parse robots.txt `body` for `adr`, storing the HTTrack group's rules; `info`
gets a disallow summary, `keep_root_disallow` FALSE drops "Disallow: /". */
gets a disallow summary, `keep_root_disallow` FALSE drops "Disallow: /", and
`sitemaps` (optional) collects the Sitemap: URLs, one per line. */
void robots_parse(robots_wizard *robots, const char *adr, const char *body,
size_t bodysize, char *info, size_t infosize,
hts_boolean keep_root_disallow);
hts_boolean keep_root_disallow, char *sitemaps,
size_t sitemapsize);
#endif
#endif

View File

@@ -33,6 +33,7 @@ Please visit our Website: http://www.httrack.com
#ifndef HTSSAFE_DEFH
#define HTSSAFE_DEFH
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -459,6 +460,115 @@ static HTS_INLINE HTS_UNUSED const char *htsbuff_str(const htsbuff *b) {
return b->buf;
}
/**
* Copy src into dest (capacity size, NUL included), truncating to fit and
* always NUL-terminating. Unlike strlcpybuff() it never aborts, so it suits a
* value read back from a cache, a header or the wire, where refusing the whole
* record is worse than keeping a clipped one. Returns HTS_TRUE if it all fit;
* callers that clip on purpose ignore that, so it is not HTS_CHECK_RESULT.
*/
static HTS_INLINE HTS_UNUSED hts_boolean strclipbuff(char *dest, size_t size,
const char *src) {
size_t len, copy;
assertf(dest != NULL && src != NULL && size != 0);
len = strlen(src);
copy = len < size ? len : size - 1;
memcpy(dest, src, copy);
dest[copy] = '\0';
return copy == len ? HTS_TRUE : HTS_FALSE;
}
/**
* Callers that deliberately ignore truncation use this instead of
* slprintfbuff(), so it is not HTS_CHECK_RESULT.
*/
static HTS_INLINE HTS_UNUSED HTS_PRINTF_FUN(3, 0) hts_boolean
vslprintfbuff(char *dest, size_t size, const char *fmt, va_list args) {
int ret;
assertf(dest != NULL && size != 0);
/* a vsnprintf failing outright may write nothing at all, leaving whatever
the caller had on the stack for it to publish */
dest[0] = '\0';
ret = vsnprintf(dest, size, fmt, args);
/* pre-C99 runtimes (msvcrt _vsnprintf) return -1 and do not terminate */
dest[size - 1] = '\0';
return ret >= 0 && (size_t) ret < size ? HTS_TRUE : HTS_FALSE;
}
/**
* Formatted print into dest (capacity size, NUL included), truncating to fit
* and always NUL-terminating. Returns HTS_TRUE if the whole output fit; the
* result is the only truncation signal, so it must be acted on. Unlike
* strcpybuff() it never aborts, so it suits text built from remote input.
*/
static HTS_INLINE HTS_UNUSED HTS_CHECK_RESULT HTS_PRINTF_FUN(3, 4) hts_boolean
slprintfbuff(char *dest, size_t size, const char *fmt, ...) {
va_list args;
hts_boolean ret;
va_start(args, fmt);
ret = vslprintfbuff(dest, size, fmt, args);
va_end(args);
return ret;
}
/**
* slprintfbuff() for diagnostics quoting remote or client text, which are
* meant to be clipped: nothing to act on, hence not HTS_CHECK_RESULT. A (void)
* cast on slprintfbuff() is no substitute, GCC warns through it.
*/
static HTS_INLINE HTS_UNUSED HTS_PRINTF_FUN(3, 4) void slprintfbuff_clip(
char *dest, size_t size, const char *fmt, ...) {
va_list args;
va_start(args, fmt);
(void) vslprintfbuff(dest, size, fmt, args);
va_end(args);
}
/**
* slprintfbuff() over the in-scope array ARR (capacity = sizeof(ARR)).
* On GCC/Clang a pointer is a compile error; use slprintfbuff() for those.
*/
#if (defined(__GNUC__) && !defined(__cplusplus))
#define sprintfbuff(ARR, ...) \
slprintfbuff((ARR), sizeof(ARR) + htsbuff_must_be_array_(ARR), __VA_ARGS__)
#else
#define sprintfbuff(ARR, ...) slprintfbuff((ARR), sizeof(ARR), __VA_ARGS__)
#endif
/* Last character of s, or '\0' when s is empty. Replaces s[strlen(s) - 1],
which indexes one byte before the buffer on an empty string. */
static HTS_INLINE HTS_UNUSED char hts_lastchar(const char *s) {
const size_t len = strlen(s);
return len != 0 ? s[len - 1] : '\0';
}
/* Drop a trailing c from s if present; HTS_TRUE if one was dropped. */
static HTS_INLINE HTS_UNUSED hts_boolean hts_striplastchar(char *s, char c) {
const size_t len = strlen(s);
if (len != 0 && s[len - 1] == c) {
s[len - 1] = '\0';
return HTS_TRUE;
}
return HTS_FALSE;
}
/* Drop the last character of s whatever it is; HTS_TRUE if s was not empty. */
static HTS_INLINE HTS_UNUSED hts_boolean hts_choplastchar(char *s) {
const size_t len = strlen(s);
if (len != 0) {
s[len - 1] = '\0';
return HTS_TRUE;
}
return HTS_FALSE;
}
/* Thin aliases over the libc allocator/memcpy (historical "t" suffix); no
added bounds checking. freet() also NULLs the freed pointer and tolerates
NULL. memcpybuff() despite the name is a raw memcpy: the caller owns the

File diff suppressed because it is too large Load Diff

View File

@@ -318,6 +318,18 @@ typedef struct {
error_redirect = "/server/error.html"; \
} while(0)
/* Longest error message shown on the error page; the rest is clipped. */
#define ERROR_MESSAGE_MAX 1024
/* SET_ERROR() with a printf format. Clips: these messages quote posted fields,
whose length the client picks. */
#define SET_ERRORF(...) \
do { \
char errbuf[ERROR_MESSAGE_MAX]; \
slprintfbuff_clip(errbuf, sizeof(errbuf), __VA_ARGS__); \
SET_ERROR(errbuf); \
} while (0)
/* Longest "sid" value worth unescaping: the expected one is an md5 hex digest,
so anything near this is already invalid and is rejected unread. */
#define SID_VALUE_MAX 64
@@ -363,6 +375,59 @@ static hts_boolean body_sid_is_valid(const char *body, const char *expected) {
return seen;
}
/** Append src to the NUL-terminated dst of capacity size (NUL included).
False, leaving dst untouched, if it would not fit: unlike strcatbuff() this
never aborts, because every piece appended here is client-supplied. */
static hts_boolean path_append(char *dst, size_t size, const char *src) {
const size_t used = strlen(dst);
const size_t len = strlen(src);
/* dst holds at most size-1 bytes, so "size - used" is >= 1 and the untrusted
len stays alone: "used + len < size" could wrap and pass. */
if (len >= size - used) {
return HTS_FALSE;
}
memcpy(dst + used, src, len + 1);
return HTS_TRUE;
}
/* Append c to dst as an HTML entity, or return HTS_FALSE if it needs none. */
static hts_boolean cat_html_escaped(String *dst, char c) {
switch (c) {
case '<':
StringCat(*dst, "&lt;");
break;
case '>':
StringCat(*dst, "&gt;");
break;
case '&':
StringCat(*dst, "&amp;");
break;
case '\'':
StringCat(*dst, "&#39;");
break;
default:
return HTS_FALSE;
}
return HTS_TRUE;
}
/* Append the value of a double-quoted command-line argument: escaped for HTML,
which the browser undoes when it posts the command line back, and for the
argv splitter, which does not. */
static void cat_cmdline_arg(String *output, const char *value) {
const char *a;
for (a = value; *a != '\0'; a++) {
if (*a == '\\' || *a == '\"') {
StringCat(*output, "\\");
}
if (!cat_html_escaped(output, *a)) {
StringMemcat(*output, a, 1);
}
}
}
int smallserver(T_SOC soc, char *url, char *method, char *data, char *path) {
int timeout = 30;
int retour = 0;
@@ -374,6 +439,9 @@ int smallserver(T_SOC soc, char *url, char *method, char *data, char *path) {
String tmpbuff = STRING_EMPTY;
String tmpbuff2 = STRING_EMPTY;
String fspath = STRING_EMPTY;
/* Project directory this server set up; the only root /website/ serves from,
and deliberately not cleared between requests. */
String website = STRING_EMPTY;
char catbuff[CATBUFF_SIZE];
/* Load strings */
@@ -669,7 +737,7 @@ int smallserver(T_SOC soc, char *url, char *method, char *data, char *path) {
fp = fopen(StringBuff(fspath), "rb");
if (fp) {
/* Read file */
while(!feof(fp)) {
while (!feof(fp) && !ferror(fp)) {
char *str = line;
char *pos;
@@ -785,6 +853,11 @@ int smallserver(T_SOC soc, char *url, char *method, char *data, char *path) {
if (!structcheck(StringBuff(tmpbuff))) {
FILE *fp;
/* Both halves of fspath come from posted fields, so a ".."
in them would escape the mirror once served. */
if (strstr(StringBuff(fspath), "..") == NULL) {
StringCopy(website, StringBuff(fspath));
}
StringCat(tmpbuff, "winprofile.ini");
fp = fopen(StringBuff(tmpbuff), "wb");
if (fp != NULL) {
@@ -818,28 +891,18 @@ int smallserver(T_SOC soc, char *url, char *method, char *data, char *path) {
commandEnd = 1;
}
} else {
char tmp[1024];
sprintf(tmp,
"Unable to write %d bytes in the the init file %s",
count, StringBuff(fspath));
SET_ERROR(tmp);
SET_ERRORF(
"Unable to write %d bytes in the the init file %s",
count, StringBuff(fspath));
}
fclose(fp);
} else {
char tmp[1024];
sprintf(tmp, "Unable to create the init file %s",
StringBuff(fspath));
SET_ERROR(tmp);
SET_ERRORF("Unable to create the init file %s",
StringBuff(fspath));
}
} else {
char tmp[1024];
sprintf(tmp,
"Unable to create the directory structure in %s",
StringBuff(fspath));
SET_ERROR(tmp);
SET_ERRORF("Unable to create the directory structure in %s",
StringBuff(fspath));
}
} else {
@@ -857,7 +920,7 @@ int smallserver(T_SOC soc, char *url, char *method, char *data, char *path) {
/* Response */
if (meth) {
int virtualpath = 0;
hts_boolean virtualpath = HTS_FALSE;
char *pos;
char *url = strchr(line1, ' ');
@@ -868,11 +931,11 @@ int smallserver(T_SOC soc, char *url, char *method, char *data, char *path) {
char *qpos;
/* get the URL */
fsfile[0] = '\0';
if (error_redirect == NULL) {
if ((qpos = strchr(url, '?'))) {
*qpos = '\0';
}
fsfile[0] = '\0';
if (strcmp(url, "/") == 0) {
file = "/server/index.html";
meth = 2;
@@ -885,7 +948,7 @@ int smallserver(T_SOC soc, char *url, char *method, char *data, char *path) {
}
if (strncmp(file, "/website/", 9) == 0) {
virtualpath = 1;
virtualpath = HTS_TRUE;
}
/* override */
@@ -899,20 +962,28 @@ int smallserver(T_SOC soc, char *url, char *method, char *data, char *path) {
}
}
if (strlen(path) + strlen(file) + 32 < sizeof(fsfile)) {
if (strncmp(file, "/website/", 9) != 0) {
sprintf(fsfile, "%shtml%s", path, file);
} else {
intptr_t adr = 0;
/* the override above may have swapped a mirror path for a GUI page */
virtualpath = strncmp(file, "/website/", 9) == 0;
if (coucal_readptr(NewLangList, "projpath", &adr)) {
sprintf(fsfile, "%s%s", (char *) adr, file + 9);
}
if (!virtualpath) {
if (!path_append(fsfile, sizeof(fsfile), path) ||
!path_append(fsfile, sizeof(fsfile), "html") ||
!path_append(fsfile, sizeof(fsfile), file)) {
fsfile[0] = '\0';
}
} else if (StringNotEmpty(website)) {
/* Never the posted "projpath": a client root reads any file. */
if (!path_append(fsfile, sizeof(fsfile), StringBuff(website)) ||
!path_append(fsfile, sizeof(fsfile), "/") ||
!path_append(fsfile, sizeof(fsfile), file + 9)) {
fsfile[0] = '\0';
}
}
if (fsfile[0] && strstr(file, "..") == NULL
&& (fp = fopen(fsfile, "rb"))) {
/* Regular files only: reading a directory or FIFO never ends, and
"path" may hold "..", so only the untrusted halves are checked. */
if (fsfile[0] && strstr(file, "..") == NULL && fexist(fsfile) &&
(fp = fopen(fsfile, "rb"))) {
char ok[] =
"HTTP/1.0 200 OK\r\n" "Connection: close\r\n"
"Server: httrack-small-server\r\n" "Content-type: text/html\r\n"
@@ -965,11 +1036,13 @@ int smallserver(T_SOC soc, char *url, char *method, char *data, char *path) {
StringCat(headers, "\r\n");
}
coucal_write(NewLangList, "redirect", (intptr_t) NULL);
} else if (is_html(file)) {
} else if (!virtualpath && is_html(file)) {
/* GUI templates only: ${_sid} in a mirrored page would hand the
crawled site the session id that authenticates commands */
int outputmode = 0;
StringMemcat(headers, ok, sizeof(ok) - 1);
while(!feof(fp)) {
while (!feof(fp) && !ferror(fp)) {
char *str = line;
int prevlen = (int) StringLength(output);
int nocr = 0;
@@ -977,9 +1050,9 @@ int smallserver(T_SOC soc, char *url, char *method, char *data, char *path) {
if (!linput(fp, line, sizeof(line) - 2)) {
*str = '\0';
}
if (*str && str[strlen(str) - 1] == '\\') {
if (hts_lastchar(str) == '\\') {
nocr = 1;
str[strlen(str) - 1] = '\0';
hts_striplastchar(str, '\\');
}
while(*str) {
char *pos;
@@ -993,6 +1066,7 @@ int smallserver(T_SOC soc, char *url, char *method, char *data, char *path) {
int p;
int format = 0;
int listDefault = 0;
hts_boolean unquoted = HTS_FALSE;
name[0] = '\0';
strlncatbuff(name, str, sizeof(name_), n);
@@ -1002,6 +1076,12 @@ int smallserver(T_SOC soc, char *url, char *method, char *data, char *path) {
} else if ((p = strfield(name, "html:"))) {
name += p;
format = 1;
} else if ((p = strfield(name, "unquoted:"))) {
name += p;
unquoted = HTS_TRUE;
} else if ((p = strfield(name, "arg:"))) {
name += p;
format = 5;
} else if ((p = strfield(name, "list:"))) {
name += p;
format = 2;
@@ -1089,11 +1169,8 @@ int smallserver(T_SOC soc, char *url, char *method, char *data, char *path) {
char *rpath = (char *) adr;
//find_handle h;
if (rpath[0]) {
if (rpath[strlen(rpath) - 1] == '/') {
rpath[strlen(rpath) - 1] = '\0'; /* note: patching stored (inhash) value */
}
}
/* note: patching stored (inhash) value */
hts_striplastchar(rpath, '/');
{
const char *profiles = hts_getcategories(rpath, 0);
const char *categ = hts_getcategories(rpath, 1);
@@ -1138,8 +1215,8 @@ int smallserver(T_SOC soc, char *url, char *method, char *data, char *path) {
test:<if ==0>:<if ==1>:<if == 2>..
ztest:<if == 0 || !exist>:<if == 1>:<if == 2>..
*/
else if ((p = strfield(name, "test:"))
|| (p = strfield(name, "ztest:"))) {
else if ((p = strfield(name, "test:")) ||
(p = strfield(name, "ztest:"))) {
intptr_t adr = 0;
char *pos2;
int ztest = (name[0] == 'z');
@@ -1242,6 +1319,12 @@ int smallserver(T_SOC soc, char *url, char *method, char *data, char *path) {
}
}
}
/* consumed here: it shares nothing with the list and
option formats below */
if (format == 5 && langstr != NULL && outputmode != -1) {
cat_cmdline_arg(&output, langstr);
langstr = NULL;
}
if (langstr && outputmode != -1) {
switch (format) {
case 0:
@@ -1259,18 +1342,18 @@ int smallserver(T_SOC soc, char *url, char *method, char *data, char *path) {
StringMemcat(output, &c, 1);
}
a += 2;
} else if (outputmode && a[0] == '<') {
StringCat(output, "&lt;");
} else if (outputmode && a[0] == '>') {
StringCat(output, "&gt;");
} else if (outputmode && a[0] == '&') {
StringCat(output, "&amp;");
} else if (outputmode && a[0] == '\'') {
StringCat(output, "&#39;");
} else if (unquoted && a[0] == '\"') {
/* the browser posts an entity back as a raw
quote, which would open a quoted run in the
argv splitter; a URI cannot hold one anyway */
StringCat(output, "%22");
} else if (outputmode &&
cat_html_escaped(&output, a[0])) {
/* appended as an entity */
} else if (outputmode == 3 && a[0] == ' ') {
StringCat(output, "%20");
} else if (outputmode >= 2
&& ((unsigned char) a[0]) < 32) {
} else if (outputmode >= 2 &&
((unsigned char) a[0]) < 32) {
char tmp[32];
sprintf(tmp, "%%%02x", (unsigned char) a[0]);
@@ -1331,20 +1414,10 @@ int smallserver(T_SOC soc, char *url, char *method, char *data, char *path) {
}
StringClear(tmpbuff);
break;
case '<':
StringCat(tmpbuff, "&lt;");
break;
case '>':
StringCat(tmpbuff, "&gt;");
break;
case '&':
StringCat(tmpbuff, "&amp;");
break;
case '\'':
StringCat(tmpbuff, "&#39;");
break;
default:
StringMemcat(tmpbuff, fstr, 1);
if (!cat_html_escaped(&tmpbuff, *fstr)) {
StringMemcat(tmpbuff, fstr, 1);
}
break;
}
fstr++;
@@ -1384,7 +1457,9 @@ int smallserver(T_SOC soc, char *url, char *method, char *data, char *path) {
}
#endif
} else {
if (is_text(file)) {
if (is_html(file)) {
StringMemcat(headers, ok, sizeof(ok) - 1);
} else if (is_text(file)) {
StringMemcat(headers, ok_text, sizeof(ok_text) - 1);
} else if (is_js(file)) {
StringMemcat(headers, ok_js, sizeof(ok_js) - 1);
@@ -1398,14 +1473,15 @@ int smallserver(T_SOC soc, char *url, char *method, char *data, char *path) {
while(!feof(fp)) {
int n = (int) fread(line, 1, sizeof(line) - 2, fp);
if (n > 0) {
StringMemcat(output, line, n);
if (n <= 0) {
break; /* short read: EOF or error, never a retry */
}
StringMemcat(output, line, n);
}
}
fclose(fp);
} else if (strcmp(file, "/ping") == 0
|| strncmp(file, "/ping?", 6) == 0) {
} else if (strcmp(file, "/ping") == 0 ||
strncmp(file, "/ping?", 6) == 0) {
char error_hdr[] =
"HTTP/1.0 200 Pong\r\n" "Server: httrack small server\r\n"
"Content-type: text/html\r\n";
@@ -1491,6 +1567,7 @@ int smallserver(T_SOC soc, char *url, char *method, char *data, char *path) {
StringFree(tmpbuff);
StringFree(tmpbuff2);
StringFree(fspath);
StringFree(website);
if (buffer)
free(buffer);

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