Compare commits

..

25 Commits

Author SHA1 Message Date
Xavier Roche
9fbdc19126 Merge fix/cache-repair-rename into fix/refetch-backup-name
Carries the here-string reconciliation with #822 and the aside
directory-refusal fix. TESTS tail keeps all four new entries in order.

Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-27 16:07:28 +02:00
Xavier Roche
c31f53132f Merge fix/rename-over-aside into fix/cache-repair-rename
Carries the here-string reconciliation with #822 and the aside
directory-refusal fix. TESTS tail keeps both new entries in order.

Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-27 16:05:18 +02:00
Xavier Roche
1b3f7918b6 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>
2026-07-27 16:03:13 +02:00
Xavier Roche
aeef177267 Merge origin/master into fix/rename-over-aside
#822 converted 01_engine-renameover.test's assertions to the here-string
form, since grep -q SIGPIPEs the echo feeding it and an assertion that
held reports 141. This branch had added its restore-outcome and retry
legs in the old pipe form. Resolved on master's form, with those legs
ported into it rather than kept alongside.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-27 15:59:25 +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
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
46bc313469 Reserve hts-tmp and hts-cache so no URL can name a temporary
Moving the temporaries into an hts-tmp directory only pushed the
collision one level down: hts-tmp is a path segment a URL can name, so a
site serving /d/hts-tmp/a.bin.bak still landed on the backup path for
/d/a.bin and was consumed. url_savename now escapes hts-tmp and
hts-cache as whole path components, the way it already escapes the DOS
device names, which also stops a URL from naming hts-cache/new.zip.

The three tests now drive the hostile shape rather than the pre-fix
name: a savename assertion on the escape, a crawl serving
/bakname/hts-tmp/a.bin.bak, and a selftest pinning the temporary's name.

Also: retry the backup once when another slot removed the shared
directory between structcheck and the rename; report a commit that had
to restore, so the caller does not cache the new validators against the
old body; drop the coded body on the too-long branch as its sibling
does; and scope the leftover scan to plants inside hts-tmp only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-27 15:15:12 +02:00
Xavier Roche
4446923d89 Merge branch 'fix/cache-repair-rename' into fix/refetch-backup-name 2026-07-27 14:40:46 +02:00
Xavier Roche
f39270bdd6 Merge fix/rename-over-aside, and refuse a recovery that recovered nothing
On Windows the rename destination always exists, so every cache repair
takes hts_rename_over()'s fallback; without #790 that fallback still
removes the cache before a retry that can fail. Stacking the two keeps
the repair path away from the destructive shape.

Carries the #824 fix in the same block: unzRepair() reports Z_OK with no
entries for an input that never had a local file header, and its output
can be short on a full disk, so both call sites committed an empty or
truncated archive over the cache. Both now go through one cache_repair()
that refuses a recovery holding nothing or failing to open.

Closes #824

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-27 14:40:37 +02:00
Xavier Roche
ea6aafb210 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>
2026-07-27 14:32:36 +02:00
Xavier Roche
8bad42978a Give the re-fetch backup test Windows coverage
The Windows job iterates a fixed glob, so a 108_engine-* test never ran
there. The rename semantics this drives are exactly what differs on the
CRT, which reserves EACCES for a locked source.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-27 14:15:40 +02:00
Xavier Roche
12775c2509 Merge branch 'fix/cache-repair-rename' into fix/refetch-backup-name 2026-07-27 14:15:29 +02:00
Xavier Roche
57098870d3 Give the cache-repair test Windows coverage
The Windows job iterates a fixed glob, so a 106_engine-* test never ran
there; name it in the list. The move it exercises is the one whose
failure mode is Windows-specific.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-27 14:15:22 +02:00
Xavier Roche
3ed88825f7 Merge remote-tracking branch 'origin/master' into fix/rename-over-aside 2026-07-27 14:14:14 +02:00
Xavier Roche
a61178bba7 Merge branch 'fix/cache-repair-rename' into fix/refetch-backup-name 2026-07-27 14:14:14 +02:00
Xavier Roche
6be18f430e Merge remote-tracking branch 'origin/master' into fix/cache-repair-rename 2026-07-27 14:14:14 +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
38161162e2 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>
2026-07-27 14:06:05 +02:00
Xavier Roche
8c7e0edc4e A failed filecreate() on an unknown-length re-fetch commits the backup away
The re-fetch backup was dropped on commit without checking that the new
copy exists. filecreate() can fail after the backup was taken (EMFILE,
ENOSPC, EROFS), and back_finalize()'s incomplete-transfer guard is gated
on a known Content-Length, so a chunked response went straight to the
commit and lost both copies. Restore the backup instead when there is
nothing to commit to.

Closes #775

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-27 14:06:05 +02:00
Xavier Roche
28c103db70 Cache repair deletes the old cache before a rename it never checks
Both zip-repair paths removed the damaged cache, moved repair.zip onto
its name without looking at the result, and announced a successful
recovery either way. A refused move left the entries under a name
nothing reads and no cache at all. Go through hts_rename_over(), which
keeps the destination when it cannot replace it, and report the failure
instead of claiming success.

Closes #786

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-27 14:06:05 +02:00
Xavier Roche
8cf32966fa The .bak re-fetch backup destroys a mirrored file of the same name
The re-fetch backup, the .z content-coding spool and its .u decode
target were all named by appending an extension to the local save name,
which is a name the mirror can hold. A site serving <path>.bak had its
copy renamed over as the backup and then unlinked on commit, and the run
reported success. Build them in an hts-tmp directory beside the file
instead, and remove it once the last slot sharing it is done.

Closes #774

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-27 14:06:05 +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
130 changed files with 1333 additions and 289 deletions

View File

@@ -627,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")"
@@ -669,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

@@ -211,7 +211,8 @@ jobs:
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
60_crawl-log-salvage.test 106_engine-repair-rename.test \
108_engine-refetch-backup.test; do
elapsed=$((SECONDS - started))
if [ "$elapsed" -ge "$suite_deadline" ]; then
echo "::error::suite deadline: ${elapsed}s elapsed, stopping before $t"

View File

@@ -26,12 +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.
- Never assert with `cmd | grep -q MARKER && fail`. Under `pipefail` the
pipeline is non-zero both when `cmd` fails and when `grep -q` matches early
and SIGPIPEs it, so the `&&` never fires and a probe that proved nothing reads
as "marker absent". 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.
- 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

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

@@ -777,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%">

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

@@ -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 "26 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
@@ -220,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

View File

@@ -596,6 +596,41 @@ int back_nsoc_overall(const struct_back * sback) {
return n;
}
/* Reserved subdirectory holding a mirrored file's temporaries, beside it. */
#define HTS_TMPDIR "hts-tmp"
/* Build save's temporary as <dir>/hts-tmp/<name>.<ext>. Appending the extension
to save instead put it in the mirror namespace, so a site serving <path>.bak
had its copy taken as the backup and then unlinked (#774). HTS_FALSE (dest
emptied) if it would not fit. Note: utf-8. */
static hts_boolean back_tmpname(char *dest, size_t size, const char *save,
const char *ext) {
const char *const slash = strrchr(save, '/');
const int dirlen = slash != NULL ? (int) (slash - save) + 1 : 0;
if (!slprintfbuff(dest, size, "%.*s" HTS_TMPDIR "/%s.%s", dirlen, save,
slash != NULL ? slash + 1 : save, ext)) {
dest[0] = '\0';
return HTS_FALSE;
}
return HTS_TRUE;
}
/* Note: utf-8 */
void back_tmpdir_drop(const char *tmp) {
char BIGSTK dir[HTS_URLMAXSIZE * 2];
const char *slash;
if (tmp == NULL || (slash = strrchr(tmp, '/')) == NULL)
return;
if (!strclipbuff(dir, sizeof(dir), tmp))
return;
dir[slash - tmp] = '\0';
slash = strrchr(dir, '/');
if (strcmp(slash != NULL ? slash + 1 : dir, HTS_TMPDIR) == 0)
(void) RMDIR(dir);
}
/* generate temporary file on lien_back */
/* Note: utf-8 */
static int create_back_tmpfile(httrackp *opt, lien_back *const back,
@@ -603,12 +638,10 @@ 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') {
/* 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)) {
if (!back_tmpname(back->tmpfile_buffer, sizeof(back->tmpfile_buffer),
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;
@@ -646,7 +679,11 @@ void back_refetch_backup(httrackp *opt, lien_back *const back) {
if (fexist_utf8(back->tmpfile))
hts_log_print(opt, LOG_WARNING, "replacing leftover backup %s",
back->tmpfile);
saved = hts_rename_over(back->url_sav, back->tmpfile);
saved = hts_rename_over(opt, back->url_sav, back->tmpfile);
/* Another slot sharing the directory may have removed it between the
structcheck above and the rename: recreate it and try once more. */
if (!saved && structcheck(back->tmpfile) == 0)
saved = hts_rename_over(opt, back->url_sav, back->tmpfile);
}
if (!saved) {
hts_log_print(opt, LOG_WARNING | LOG_ERRNO,
@@ -670,16 +707,22 @@ static hts_boolean back_transfer_failed(const int statuscode) {
}
}
/* Commit or restore a re-fetch backup (#77 follow-up): a re-fetch over an
existing file moved the good copy to back->tmpfile before truncating url_sav.
commit keeps the new file and drops the backup; else restore it so an aborted
transfer leaves the previous copy intact. Skips the zlib .z temp. */
static void back_finalize_backup(httrackp *opt, lien_back *const back,
const hts_boolean commit) {
hts_boolean back_finalize_backup(httrackp *opt, lien_back *const back,
hts_boolean commit) {
const hts_boolean wanted = commit;
if (back->tmpfile == NULL || back->r.compressed)
return;
return HTS_TRUE;
/* Nothing to commit to: filecreate() can fail after the backup was taken,
and dropping it then loses both copies (#775). */
if (commit && !fexist_utf8(back->url_sav)) {
hts_log_print(opt, LOG_WARNING, "%s was never created; restoring %s",
back->url_sav, back->tmpfile);
commit = HTS_FALSE;
}
if (commit) {
(void) UNLINK(back->tmpfile); /* new copy is good; drop the backup */
back_tmpdir_drop(back->tmpfile);
} else {
if (back->r.out != NULL) {
fclose(back->r.out);
@@ -687,12 +730,15 @@ 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 (!hts_rename_over(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);
else
back_tmpdir_drop(back->tmpfile);
}
back->tmpfile = NULL;
return commit == wanted ? HTS_TRUE : HTS_FALSE;
}
// objet (lien) téléchargé ou transféré depuis le cache
@@ -794,12 +840,21 @@ int back_finalize(httrackp * opt, cache_back * cache, struct_back * sback,
hts_codec_parse(back[p].r.contentencoding);
/* Never decode over url_sav: a failed decode would destroy the
copy an --update re-fetch is supposed to refresh (#557). */
char BIGSTK unpacked[HTS_URLMAXSIZE * 2 + 4]; // room for ".u"
char BIGSTK unpacked[HTS_URLMAXSIZE * 2];
LLint size;
snprintf(unpacked, sizeof(unpacked), "%s.u", back[p].url_sav);
if ((size = hts_codec_unpack(codec, back[p].tmpfile,
unpacked)) >= 0) {
/* fits whenever the .z temp it decodes from did */
if (!back_tmpname(unpacked, sizeof(unpacked), back[p].url_sav,
"u")) {
back[p].r.statuscode = STATUSCODE_INVALID;
strcpybuff(back[p].r.msg, "Error when decompressing (the "
"temporary filename is too long)");
/* as the decode-failure branch below: never let the coded
bytes be committed as the page */
if (!back[p].r.is_write)
deleteaddr(&back[p].r);
} else 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
@@ -818,7 +873,7 @@ int back_finalize(httrackp * opt, cache_back * cache, struct_back * sback,
"Read error when decompressing");
}
UNLINK(unpacked);
} else if (hts_rename_over(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);
@@ -871,6 +926,7 @@ int back_finalize(httrackp * opt, cache_back * cache, struct_back * sback,
/* ensure that no remaining temporary file exists */
if (back[p].tmpfile != NULL) {
unlink(back[p].tmpfile);
back_tmpdir_drop(back[p].tmpfile); /* the .u went with it */
back[p].tmpfile = NULL;
}
}
@@ -883,7 +939,14 @@ int back_finalize(httrackp * opt, cache_back * cache, struct_back * sback,
}
/* Body fully received: keep the freshly written url_sav, drop the
backup of the previous copy. */
back_finalize_backup(opt, &back[p], HTS_TRUE);
if (!back_finalize_backup(opt, &back[p], HTS_TRUE)) {
/* The previous copy is back because the new one was never created;
caching this response's validators against it would pin the stale
body on every later --update. */
if (fexist_utf8(back[p].url_sav))
filenote(&opt->state.strc, back[p].url_sav, NULL);
return -1;
}
/* Write mode to disk */
if (back[p].r.is_write && back[p].r.adr != NULL) {
freet(back[p].r.adr);
@@ -1719,6 +1782,7 @@ int back_clear_entry(lien_back * back) {
// only for security
if (back->tmpfile && back->tmpfile[0] != '\0') {
(void) unlink(back->tmpfile);
back_tmpdir_drop(back->tmpfile);
back->tmpfile = NULL;
}
// headers

View File

@@ -143,6 +143,18 @@ int back_finalize(httrackp * opt, cache_back * cache, struct_back * sback,
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);
/* Commit or restore a re-fetch backup (#77 follow-up): a re-fetch over an
existing file moved the good copy to back->tmpfile before truncating url_sav.
commit keeps the new file and drops the backup, unless url_sav was never
created; else restore it so an aborted transfer leaves the previous copy
intact. Skips the zlib .z temp. HTS_FALSE when a requested commit had to
restore instead: the caller then holds the OLD body and must not cache this
response's validators against it. */
hts_boolean back_finalize_backup(httrackp *opt, lien_back *const back,
hts_boolean commit);
/* Remove the reserved directory a temporary sat in, once the last slot sharing
it is done; a non-empty one just refuses. No-op outside that directory. */
void back_tmpdir_drop(const char *tmp);
/* -#test=backswap: slots eligible for the on-disk ready table. */
int back_selftest_slot_swap(void);
void back_info(struct_back * sback, int i, int j, FILE * fp);

View File

@@ -957,6 +957,35 @@ htsblk *cache_header(httrackp * opt, cache_back * cache, const char *adr,
return NULL;
}
const char *cache_repair(httrackp *opt, const char *name,
unsigned long *entries, unsigned long *bytes) {
char BIGSTK repairname[HTS_URLMAXSIZE * 2];
unzFile zip;
*entries = 0;
*bytes = 0;
if (!slprintfbuff(repairname, sizeof(repairname), "%s%s",
StringBuff(opt->path_log), "hts-cache/repair.zip"))
return "the repair path is too long";
if (unzRepair(name, repairname,
fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
StringBuff(opt->path_log), "hts-cache/repair.tmp"),
entries, bytes) != Z_OK)
return "could not repair the cache";
/* unzRepair writes an end-of-central-directory record whatever it found, so
an input holding no local file header at all yields a valid empty archive
and a short write yields a truncated one. Only an archive that holds
something and opens may replace the cache (#824). */
if (*entries == 0)
return "the repaired cache holds no entry, keeping the damaged one";
if ((zip = hts_unzOpen_utf8(repairname)) == NULL)
return "the repaired cache does not open, keeping the damaged one";
unzClose(zip);
if (!hts_rename_over(opt, repairname, name))
return "could not put the repaired cache in place";
return NULL;
}
// Initialisation du cache: créer nouveau, renomer ancien, charger..
void cache_init(cache_back * cache, httrackp * opt) {
// ---
@@ -985,6 +1014,7 @@ void cache_init(cache_back * cache, httrackp * opt) {
StringBuff(opt->path_log),
"hts-cache/new.zip")))) { // a previous cache exists.. rename it
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),
@@ -1025,8 +1055,9 @@ void cache_init(cache_back * cache, httrackp * opt) {
// Corrupted ZIP file ? Try to repair!
if (cache->zipInput == NULL && !cache->ro) {
char *name;
uLong repaired = 0;
uLong repairedBytes = 0;
const char *why;
unsigned long repaired = 0;
unsigned long repairedBytes = 0;
if (!cache->ro) {
name =
@@ -1039,25 +1070,16 @@ void cache_init(cache_back * cache, httrackp * opt) {
}
hts_log_print(opt, LOG_WARNING,
"Cache: damaged cache, trying to repair");
/* mztools has no UTF-8 hook, so repairing a corrupt cache under a
non-ASCII path_log fails cleanly (re-crawl), never forks a twin. */
if (unzRepair
(name,
fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt), StringBuff(opt->path_log),
"hts-cache/repair.zip"), fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
StringBuff(opt->path_log),
"hts-cache/repair.tmp"),
&repaired, &repairedBytes) == Z_OK) {
UNLINK(name);
RENAME(fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
StringBuff(opt->path_log), "hts-cache/repair.zip"),
name);
cache->zipInput = hts_unzOpen_utf8(name);
why = cache_repair(opt, name, &repaired, &repairedBytes);
if (why != NULL) {
hts_log_print(opt, LOG_WARNING | LOG_ERRNO, "Cache: %s", why);
} else if ((cache->zipInput = hts_unzOpen_utf8(name)) != NULL) {
hts_log_print(opt, LOG_WARNING,
"Cache: %d bytes successfully recovered in %d entries",
(int) repairedBytes, (int) repaired);
} else {
hts_log_print(opt, LOG_WARNING, "Cache: could not repair the cache");
hts_log_print(opt, LOG_WARNING,
"Cache: the repaired cache could not be reopened");
}
}
// Opened ?

View File

@@ -78,6 +78,14 @@ htsblk *cache_header(httrackp * opt, cache_back * cache, const char *adr,
const char *fil, htsblk * r);
void cache_init(cache_back * cache, httrackp * opt);
/* Recover the damaged cache at name into hts-cache/repair.zip and move it over
name, storing what was recovered in *entries and *bytes. Returns NULL on
success, else a reason the caller reports: a recovery that is empty or does
not open never replaces the cache, and neither does one that cannot be moved
into place (#786, #824). Note: utf-8. */
const char *cache_repair(httrackp *opt, const char *name,
unsigned long *entries, unsigned long *bytes);
/* Which hts-cache/ generation (new.* vs old.*) is authoritative. */
typedef enum {
CACHE_RECONCILE_PROMOTE, /* no new cache: promote the old generation */

View File

@@ -2160,8 +2160,9 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
case 'R':
{
char *name;
uLong repaired = 0;
uLong repairedBytes = 0;
const char *why;
unsigned long repaired = 0;
unsigned long repairedBytes = 0;
if (fexist_utf8(fconcat(
OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
@@ -2184,24 +2185,15 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
return 1;
}
fprintf(stderr, "Cache: trying to repair %s\n", name);
if (unzRepair
(name,
fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt), StringBuff(opt->path_log),
"hts-cache/repair.zip"),
fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt), StringBuff(opt->path_log),
"hts-cache/repair.tmp"), &repaired,
&repairedBytes) == Z_OK) {
UNLINK(name);
RENAME(fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
StringBuff(opt->path_log),
"hts-cache/repair.zip"),
name);
fprintf(stderr,
"Cache: %d bytes successfully recovered in %d entries\n",
(int) repairedBytes, (int) repaired);
} else {
fprintf(stderr, "Cache: could not repair the cache\n");
why = cache_repair(opt, name, &repaired, &repairedBytes);
if (why != NULL) {
fprintf(stderr, "Cache: %s\n", why);
return 1;
}
fprintf(
stderr,
"Cache: %d bytes successfully recovered in %d entries\n",
(int) repairedBytes, (int) repaired);
}
return 0;
break;

View File

@@ -483,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
}
@@ -617,9 +620,12 @@ 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,

View File

@@ -558,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");

View File

@@ -5790,7 +5790,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) {

View File

@@ -74,6 +74,41 @@ static const char *hts_tbdev[] = {
""
};
/* Directories the engine owns inside the mirror. A URL able to name one lands
on the cache or on another slot's temporary and destroys it (#774), so they
are escaped like the DOS devices are. */
static const char *hts_tbreserved[] = {"/hts-cache", "/hts-tmp", ""};
/* Replace /foo/<reserved>/bar by /foo/<reserved>_/bar, matching a whole path
component only (case-insensitively: the filesystem may be too). */
static void escapeReservedNames(char *save, size_t size,
const char *const *names) {
int i;
for (i = 0; names[i][0] != '\0'; i++) {
const char *a = save;
const size_t len = strlen(names[i]);
while ((a = strstrcase(a, names[i]))) {
switch ((int) a[len]) {
case '\0':
case '/':
case '.': {
char BIGSTK tempo[HTS_URLMAXSIZE * 2];
tempo[0] = '\0';
strncatbuff(tempo, save, (int) (a - save) + (int) len);
strcatbuff(tempo, "_");
strcatbuff(tempo, a + len);
/* clip rather than abort: the name comes from the wire */
(void) strclipbuff(save, size, tempo);
} break;
}
a += len;
}
}
}
/* Strip all // */
static void cleanDoubleSlash(char *s) {
int i, j;
@@ -1369,35 +1404,12 @@ int url_savename(lien_adrfilsave *const afs,
// éliminer les // (comme ftp://)
cleanDoubleSlash(afs->save);
/* Runs on every platform, and before path_html is prepended below, so the
user's own output directory is never renamed. */
escapeReservedNames(afs->save, sizeof(afs->save), hts_tbreserved);
#if HTS_OVERRIDE_DOS_FOLDERS
/* Replace /foo/nul/bar by /foo/nul_/bar */
{
int i = 0;
while(hts_tbdev[i][0]) {
const char *a = afs->save;
while((a = strstrcase(a, hts_tbdev[i]))) {
switch ((int) a[strlen(hts_tbdev[i])]) {
case '\0':
case '/':
case '.':
{
char BIGSTK tempo[HTS_URLMAXSIZE * 2];
tempo[0] = '\0';
strncatbuff(tempo, afs->save, (int) (a - afs->save) + strlen(hts_tbdev[i]));
strcatbuff(tempo, "_");
strcatbuff(tempo, a + strlen(hts_tbdev[i]));
strcpybuff(afs->save, tempo);
}
break;
}
a += strlen(hts_tbdev[i]);
}
i++;
}
}
escapeReservedNames(afs->save, sizeof(afs->save), hts_tbdev);
/* Strip ending . or ' ' forbidden on windoz */
cleanEndingSpaceOrDot(afs->save);

View File

@@ -2622,6 +2622,71 @@ static int st_savename(httrackp *opt, int argc, char **argv) {
return 0;
}
static char st_log_callback_seen[256];
static void st_log_callback(httrackp *opt, int type, const char *format,
va_list args) {
(void) opt;
(void) type;
(void) vsnprintf(st_log_callback_seen, sizeof(st_log_callback_seen), format,
args);
}
/* The callback must not consume the va_list the log file's vfprintf() needs. */
static int st_logcallback(httrackp *opt, int argc, char **argv) {
static const char want[] = "42 sentinel";
static const char want_filtered[] = "7 filtered";
char BIGSTK seen[sizeof(st_log_callback_seen)];
char BIGSTK line[256];
FILE *fp;
int rc = 1;
(void) argc;
(void) argv;
fp = tmpfile();
if (fp == NULL) {
fprintf(stderr, "logcallback: tmpfile() failed\n");
return 1;
}
opt->log = fp;
opt->debug = LOG_NOTICE;
st_log_callback_seen[0] = '\0';
hts_set_log_vprint_callback(st_log_callback);
hts_log_print(opt, LOG_NOTICE, "%d %s", 42, "sentinel");
hts_set_log_vprint_callback(NULL);
opt->log = NULL;
strcpybuff(seen, st_log_callback_seen);
rewind(fp);
line[0] = '\0';
(void) fgets(line, (int) sizeof(line), fp);
fclose(fp);
/* The callback runs above the level filter and without a log file at all;
the front-ends that install one usually have no opt->log open. */
st_log_callback_seen[0] = '\0';
hts_set_log_vprint_callback(st_log_callback);
hts_log_print(opt, LOG_DEBUG, "%d %s", 7, "filtered");
hts_set_log_vprint_callback(NULL);
/* Same arguments both ways; the file line carries a level prefix. */
if (strcmp(seen, want) != 0)
fprintf(stderr, "logcallback: callback got '%s' want '%s'\n", seen, want);
else if (strstr(line, want) == NULL)
fprintf(stderr, "logcallback: log file got '%s' want it to carry '%s'\n",
line, want);
else if (strcmp(st_log_callback_seen, want_filtered) != 0)
fprintf(stderr, "logcallback: unfiltered callback got '%s' want '%s'\n",
st_log_callback_seen, want_filtered);
else
rc = 0;
if (rc == 0)
printf("logcallback self-test OK\n");
return rc;
}
/* an empty fil started htsAddLink's codebase walk before the buffer (#730) */
static int st_addlink(httrackp *opt, int argc, char **argv) {
htsmoduleStruct BIGSTK str;
@@ -6072,10 +6137,9 @@ static hts_boolean ro_is(const char *path, const char *data) {
}
// -#test=renameover <dir>: hts_rename_over() must replace an existing dst and
// never delete one it did not replace (#779). Which half is live depends on
// never lose one it did not replace (#779, #790). Which half is live depends on
// what rename() does to an existing target, so probe that and name the regime.
static int st_renameover(httrackp *opt, int argc, char **argv) {
(void) opt;
if (argc < 1) {
fprintf(stderr, "renameover: needs a writable base dir\n");
return 1;
@@ -6104,7 +6168,7 @@ static int st_renameover(httrackp *opt, int argc, char **argv) {
ro_put(dst, "old");
if (replaceable) {
/* An existing dst must still be replaced: the unlink is for this. */
if (!hts_rename_over(src, dst)) {
if (!hts_rename_over(opt, src, dst)) {
fprintf(stderr, "renameover: replacing an existing dst failed: %s\n",
strerror(errno));
err++;
@@ -6114,7 +6178,7 @@ static int st_renameover(httrackp *opt, int argc, char **argv) {
}
} else {
/* A failure the unlink cannot fix must leave dst as it was. */
if (hts_rename_over(src, dst)) {
if (hts_rename_over(opt, src, dst)) {
fprintf(stderr, "renameover: an unfixable failure reported success\n");
err++;
}
@@ -6124,10 +6188,36 @@ static int st_renameover(httrackp *opt, int argc, char **argv) {
}
}
/* A directory in the way is not something the caller asked to replace: it
must be refused, never parked aside and orphaned. */
(void) UNLINK(dst);
ro_put(src, "new");
if (MKDIR(dst) == 0) {
char parked[sizeof(dst) + 16];
snprintf(parked, sizeof(parked), "%s.hts-old0", dst);
if (hts_rename_over(opt, src, dst)) {
fprintf(stderr, "renameover: a directory at dst reported success\n");
err++;
}
if (!ro_is(src, "new")) {
fprintf(stderr, "renameover: a directory at dst consumed src\n");
err++;
}
/* RMDIR only succeeds on a directory that is there, so it doubles as the
probe: the parked name must not exist at all. */
if (RMDIR(parked) == 0 || fexist_utf8(parked)) {
fprintf(stderr, "renameover: a directory at dst was parked aside\n");
err++;
}
(void) RMDIR(dst);
}
(void) UNLINK(src);
/* A missing src must leave dst alone and report failure. */
(void) UNLINK(src);
ro_put(dst, "keep");
if (hts_rename_over(src, dst)) {
if (hts_rename_over(opt, src, dst)) {
fprintf(stderr, "renameover: a missing src reported success\n");
err++;
}
@@ -6138,16 +6228,177 @@ static int st_renameover(httrackp *opt, int argc, char **argv) {
/* Same, with dst absent too: nothing to lose, still a failure. */
(void) UNLINK(dst);
if (hts_rename_over(src, dst)) {
if (hts_rename_over(opt, src, dst)) {
fprintf(stderr, "renameover: a missing src and dst reported success\n");
err++;
}
/* The aside fallback, driven directly: a clobbering rename() never reaches
it. Skipped in the refused regime, where no rename at all succeeds. */
if (replaceable) {
char aside[sizeof(dst) + 16], keep[sizeof(dst) + 16];
snprintf(aside, sizeof(aside), "%s.hts-old0", dst);
snprintf(keep, sizeof(keep), "%s.hts-old1", dst);
(void) UNLINK(aside);
(void) UNLINK(keep);
ro_put(src, "new");
ro_put(dst, "old");
if (!hts_rename_over_aside_selftest(opt, src, dst)) {
fprintf(stderr, "renameover: the aside fallback failed: %s\n",
strerror(errno));
err++;
} else if (!ro_is(dst, "new") || fexist_utf8(src) || fexist_utf8(aside)) {
fprintf(stderr, "renameover: the aside fallback did not replace dst\n");
err++;
}
/* #790: the retry fails (no src). The old content must survive, back at dst
or, when the move back fails too, under the parked name it is logged as.
Name the outcome so a leg cannot pass having tested the other one. */
(void) UNLINK(src);
ro_put(dst, "old");
if (hts_rename_over_aside_selftest(opt, src, dst)) {
fprintf(stderr, "renameover: a failed aside retry reported success\n");
err++;
}
if (ro_is(dst, "old") && !fexist_utf8(aside)) {
printf("renameover: restore back\n");
} else if (ro_is(aside, "old") && !fexist_utf8(dst)) {
printf("renameover: restore parked\n");
(void) UNLINK(aside);
ro_put(dst, "old");
} else {
fprintf(stderr, "renameover: a failed aside retry lost the old copy\n");
err++;
}
/* An unrelated file already sitting on the aside name must survive. */
ro_put(src, "new");
ro_put(aside, "mine");
if (!hts_rename_over_aside_selftest(opt, src, dst)) {
fprintf(stderr, "renameover: a taken aside name failed the move: %s\n",
strerror(errno));
err++;
} else if (!ro_is(dst, "new") || !ro_is(aside, "mine") ||
fexist_utf8(keep)) {
fprintf(stderr, "renameover: a taken aside name was not skipped\n");
err++;
}
(void) UNLINK(aside);
(void) UNLINK(keep);
/* A directory there reads as free to the probe, so the park must skip it
on the refusal rather than give up. */
ro_put(src, "new");
ro_put(dst, "old");
if (MKDIR(aside) == 0) {
if (!hts_rename_over_aside_selftest(opt, src, dst)) {
fprintf(stderr,
"renameover: a directory on the aside name blocked the "
"move: %s\n",
strerror(errno));
err++;
} else if (!ro_is(dst, "new") || fexist_utf8(keep)) {
fprintf(stderr, "renameover: a directory on the aside name was not "
"skipped\n");
err++;
}
(void) RMDIR(aside);
}
(void) UNLINK(keep);
}
(void) UNLINK(src);
(void) UNLINK(dst);
printf("renameover: %s\n", err ? "FAIL" : "OK");
return err;
}
// -#test=refetchbackup <dir>: the #77 re-fetch backup must build its temporary
// inside the reserved hts-tmp directory, whose segment url_savename escapes so
// no mirrored file can ever sit there (#774), and must never leave the resource
// without a copy (#775).
static int st_refetchbackup(httrackp *opt, int argc, char **argv) {
lien_back *back;
char want[HTS_URLMAXSIZE * 2 + 32];
int err = 0;
if (argc < 1) {
fprintf(stderr, "refetchbackup: needs a writable base dir\n");
return 1;
}
back = calloct(1, sizeof(lien_back));
if (back == NULL) {
fprintf(stderr, "refetchbackup: out of memory\n");
return 1;
}
/* explicit separator: fconcat() joins without one, which would put the
temporary in the parent of the directory under test */
snprintf(back->url_sav, sizeof(back->url_sav), "%s/refetch.bin", argv[0]);
snprintf(want, sizeof(want), "%s/hts-tmp/refetch.bin.bak", argv[0]);
/* #774: pin the name, so moving the temporary out of the reserved directory
cannot pass without url_savename reserving wherever it went instead. */
ro_put(back->url_sav, "old");
back_refetch_backup(opt, back);
if (back->tmpfile == NULL || fexist_utf8(back->url_sav)) {
fprintf(stderr, "refetchbackup: the previous copy was not moved aside\n");
err++;
} else if (strcmp(back->tmpfile, want) != 0) {
fprintf(stderr, "refetchbackup: temporary is %s, want %s\n", back->tmpfile,
want);
err++;
}
ro_put(back->url_sav, "new"); /* what filecreate() + the transfer produce */
back_finalize_backup(opt, back, HTS_TRUE);
if (!ro_is(back->url_sav, "new")) {
fprintf(stderr, "refetchbackup: the committed copy is not the new one\n");
err++;
}
/* #758: only a killed run can leave something there, and it must be replaced
rather than disable the backup for good. */
if (structcheck(want) != 0) {
fprintf(stderr, "refetchbackup: cannot create %s\n", want);
freet(back);
return 1;
}
ro_put(want, "leftover");
back_refetch_backup(opt, back);
if (back->tmpfile == NULL || !ro_is(want, "new")) {
fprintf(stderr, "refetchbackup: a leftover temporary blocked the backup\n");
err++;
}
/* #775: filecreate() failed, so there is nothing to commit to. Saying so is
load-bearing: the caller must not cache this response against the old
body, or the next --update gets a 304 pinning it. */
(void) UNLINK(back->url_sav);
if (back_finalize_backup(opt, back, HTS_TRUE)) {
fprintf(stderr, "refetchbackup: a commit that restored reported success\n");
err++;
}
if (!ro_is(back->url_sav, "new")) {
fprintf(stderr, "refetchbackup: a commit with no new copy lost both\n");
err++;
}
/* An aborted transfer restores, as before. */
back_refetch_backup(opt, back);
ro_put(back->url_sav, "partial");
back_finalize_backup(opt, back, HTS_FALSE);
if (!ro_is(back->url_sav, "new")) {
fprintf(stderr, "refetchbackup: an aborted re-fetch kept the partial\n");
err++;
}
(void) UNLINK(back->url_sav);
freet(back);
printf("refetchbackup: %s\n", err ? "FAIL" : "OK");
return err;
}
// -#test=direnum <dir>: enumerate a long+non-ASCII directory via the
// opendir/readdir wrappers; children must round-trip as UTF-8 (#133,#630).
static int st_direnum(httrackp *opt, int argc, char **argv) {
@@ -6824,6 +7075,8 @@ static const struct selftest_entry {
st_growsize},
{"addlink", "", "htsAddLink codebase walk over an empty current path",
st_addlink},
{"logcallback", "", "log callback must not consume the log file's va_list",
st_logcallback},
{"cache", "<dir>", "cache read/write round-trip self-test", st_cache},
{"cacheindex", "", "cache-index (.ndx) parse must stay in bounds",
st_cacheindex},
@@ -6896,6 +7149,9 @@ static const struct selftest_entry {
"hts_rename_over(): replace dst, but never delete a dst it did not "
"replace",
st_renameover},
{"refetchbackup", "<dir>",
"the re-fetch backup always leaves a copy, and stays out of the mirror",
st_refetchbackup},
{"direnum", "<dir>",
"enumerate a long+non-ASCII directory through opendir/readdir",
st_direnum},

View File

@@ -1109,7 +1109,7 @@ hts_boolean singlefile_rewrite_file(httrackp *opt, const char *root,
HTS_ACCESS_FILE);
#endif
if (ok)
ok = hts_rename_over(StringBuff(tmp), page_path);
ok = hts_rename_over(opt, StringBuff(tmp), page_path);
if (!ok) {
hts_log_print(opt, LOG_ERROR, "single-file: could not rewrite %s",
page_path);

View File

@@ -1438,21 +1438,75 @@ HTSEXT_API hts_boolean hts_findissystem(find_handle find) {
return 0;
}
hts_boolean hts_rename_over(const char *src, const char *dst) {
/* Park cdst under a free sibling name; caside receives it. */
static hts_boolean rename_park_aside(char *caside, size_t size,
const char *cdst) {
int i;
for (i = 0; i < 16; i++) {
if (!slprintfbuff(caside, size, "%s.hts-old%d", cdst, i))
return HTS_FALSE;
/* Skip a name the mirror already holds: POSIX rename() would clobber it
(#774). A non-regular entry reads as free and the rename refuses it. */
if (fexist_utf8(caside))
continue;
if (RENAME(cdst, caside) == 0)
return HTS_TRUE;
}
return HTS_FALSE;
}
/* cdst is in the way of the move: park it, retry, and put it back if the retry
fails too. Unlinking it instead would leave nothing at all (#790). */
static hts_boolean rename_over_aside(httrackp *opt, const char *csrc,
const char *cdst) {
char caside[CATBUFF_SIZE];
int err;
/* Only a regular file may be parked: a directory in the way is not what the
caller asked to replace, and parking it orphans it (UNLINK cannot drop). */
if (!fexist_utf8(cdst))
return HTS_FALSE;
if (!rename_park_aside(caside, sizeof(caside), cdst))
return HTS_FALSE;
if (RENAME(csrc, cdst) == 0) {
(void) UNLINK(caside);
return HTS_TRUE;
}
err = errno;
/* Retry once, then name the parked copy: nothing else on disk or in the log
points at it, and an --update purge would delete it unnoticed. */
if (RENAME(caside, cdst) != 0 && RENAME(caside, cdst) != 0)
hts_log_print(opt, LOG_WARNING | LOG_ERRNO,
"could not put %s back; its previous content is now %s", cdst,
caside);
errno = err;
return HTS_FALSE;
}
hts_boolean hts_rename_over(httrackp *opt, const char *src, const char *dst) {
char csrc[CATBUFF_SIZE], cdst[CATBUFF_SIZE];
fconv(csrc, sizeof(csrc), src);
fconv(cdst, sizeof(cdst), dst);
if (RENAME(csrc, cdst) == 0)
return HTS_TRUE;
/* Only a dst in the way is something the unlink can clear, and the CRT maps
that to EEXIST; it keeps EACCES for a src another process holds, where
removing dst would lose a file the retry cannot replace (#790). The src
check covers a CRT that reports neither. */
/* Only a dst in the way is something the fallback can clear, and the CRT maps
that to EEXIST; it keeps EACCES for a src another process holds, where the
retry would fail the same way. The src check covers a CRT that reports
neither. */
const int err = errno;
if (err != EEXIST || !fexist_utf8(src))
return HTS_FALSE;
(void) UNLINK(cdst);
return RENAME(csrc, cdst) == 0 ? HTS_TRUE : HTS_FALSE;
return rename_over_aside(opt, csrc, cdst);
}
hts_boolean hts_rename_over_aside_selftest(httrackp *opt, const char *src,
const char *dst) {
char csrc[CATBUFF_SIZE], cdst[CATBUFF_SIZE];
fconv(csrc, sizeof(csrc), src);
fconv(cdst, sizeof(cdst), dst);
return rename_over_aside(opt, csrc, cdst);
}

View File

@@ -137,11 +137,17 @@ HTSEXT_API hts_boolean hts_findisdir(find_handle find);
HTSEXT_API hts_boolean hts_findisfile(find_handle find);
HTSEXT_API hts_boolean hts_findissystem(find_handle find);
/* Move src onto dst, replacing an existing dst; HTS_TRUE on success. Both
paths are fconv()'d. dst is removed only to make room for a src that exists,
so a caller whose src was never written keeps its dst; a retry that still
fails does not (#790). */
hts_boolean hts_rename_over(const char *src, const char *dst);
/* Move src onto dst, replacing an existing dst; HTS_TRUE on success. Both paths
are fconv()'d. A dst in the way is parked under a sibling name rather than
removed, so the old content survives a failure: back at dst, or under that
sibling (named in the log) when the move back failed too. Not atomic: a crash
between the two renames leaves dst absent and its content beside it. */
hts_boolean hts_rename_over(httrackp *opt, const char *src, const char *dst);
/* Selftest hook: run the aside fallback directly, on a platform whose rename()
never reaches it. Both paths are fconv()'d. */
hts_boolean hts_rename_over_aside_selftest(httrackp *opt, const char *src,
const char *dst);
#endif

View File

@@ -38,6 +38,7 @@ Please visit our Website: http://www.httrack.com
#include "htscore.h"
#include "htslib.h"
#include "htsback.h"
#include "htstools.h"
#include "htssafe.h"
#include "htszlib.h"
@@ -1114,7 +1115,7 @@ static void warc_wacz_package(warc_writer *w) {
hts_log_print(w->opt, LOG_WARNING,
"WACZ: packaging failed, kept existing %s untouched",
waczpath);
} else if (!hts_rename_over(tmppath, waczpath)) {
} else if (!hts_rename_over(w->opt, tmppath, waczpath)) {
(void) UNLINK(fconv(catbuff, sizeof(catbuff), tmppath));
hts_log_print(w->opt, LOG_WARNING | LOG_ERRNO,
"WACZ: could not finalize %s", waczpath);
@@ -1354,6 +1355,7 @@ void warc_free_request(htsblk *r) {
freet(r->warc_resphdr);
if (r->warc_rawpath != NULL) {
(void) UNLINK(r->warc_rawpath); /* owns the verbatim spool file */
back_tmpdir_drop(r->warc_rawpath);
freet(r->warc_rawpath);
r->warc_rawpath = NULL;
}
@@ -1589,7 +1591,7 @@ static hts_boolean warc_commit(warc_writer *w) {
for (s = 0; s < nseg; s++) {
const char *final = warc_seg_path(w, s, finalbuf, sizeof(finalbuf));
snprintf(tmpbuf, sizeof(tmpbuf), "%s" WARC_TMP_SUFFIX, final);
if (!hts_rename_over(tmpbuf, final)) {
if (!hts_rename_over(w->opt, tmpbuf, final)) {
hts_log_print(w->opt, LOG_ERROR | LOG_ERRNO,
"WARC: could not replace %s", final);
return HTS_FALSE;

View File

@@ -4,4 +4,5 @@
set -euo pipefail
httrack -O /dev/null -#test=addlink | grep -q "addlink self-test OK"
out=$(httrack -O /dev/null -#test=addlink)
grep -q "addlink self-test OK" <<<"$out"

View File

@@ -5,4 +5,5 @@ set -euo pipefail
# A ready slot still owning a temporary must stay in memory: swapping it out
# clears the entry, which unlinks the re-fetch backup (#771).
httrack -O /dev/null -#test=backswap | grep -q "backswap self-test: OK"
out=$(httrack -O /dev/null -#test=backswap)
grep -q "backswap self-test: OK" <<<"$out"

View File

@@ -4,7 +4,8 @@
set -euo pipefail
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/httrack_changes_st.XXXXXX") || exit 1
trap 'rm -rf "$tmpdir"' EXIT HUP INT QUIT PIPE TERM
trap 'set +e; rm -rf "$tmpdir"' EXIT
trap 'rm -rf "$tmpdir"' HUP INT QUIT PIPE TERM
# No pipe into grep: SIGPIPE would mask a failing exit status.
expect_ok() {

View File

@@ -5,4 +5,5 @@ set -euo pipefail
# webhttrack posts its command line as one string: the argv split must grow past
# 1024 arguments and keep a quote inside a value out of the option parser.
httrack -O /dev/null -#test=cmdline-split run | grep -q "cmdline-split self-test OK"
out=$(httrack -O /dev/null -#test=cmdline-split run)
grep -q "cmdline-split self-test OK" <<<"$out"

View File

@@ -11,7 +11,8 @@
set -euo pipefail
tmp=$(mktemp -d "${TMPDIR:-/tmp}/httrack_cmdline.XXXXXX") || exit 1
trap 'rm -rf "$tmp"' EXIT HUP INT QUIT PIPE TERM
trap 'set +e; rm -rf "$tmp"' EXIT
trap 'rm -rf "$tmp"' HUP INT QUIT PIPE TERM
echo '<html><body>hello</body></html>' >"$tmp/index.html"

View File

@@ -7,6 +7,7 @@ set -euo pipefail
# cookies *@*.txt) from a long, non-ASCII folder through the UTF-8/long-path
# file wrappers (#133,#630).
dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT
trap 'set +e; rm -rf "$dir"' EXIT
httrack -O /dev/null -#test=cookieimport "$dir" | grep -q "cookieimport:.*OK"
out=$(httrack -O /dev/null -#test=cookieimport "$dir")
grep -q "cookieimport:.*OK" <<<"$out"

View File

@@ -6,6 +6,7 @@ set -euo pipefail
# Drives -#test=direnum: enumerate a long+non-ASCII directory through the
# opendir/readdir wrappers, checking each child round-trips as UTF-8 (#133,#630).
dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT
trap 'set +e; rm -rf "$dir"' EXIT
httrack -O /dev/null -#test=direnum "$dir" | grep -q "direnum:.*OK"
out=$(httrack -O /dev/null -#test=direnum "$dir")
grep -q "direnum:.*OK" <<<"$out"

View File

@@ -28,7 +28,8 @@ case "$bin" in
esac
tmp=$(mktemp -d "${TMPDIR:-/tmp}/httrack_doitlog.XXXXXX") || exit 1
trap 'rm -rf "$tmp"' EXIT HUP INT QUIT PIPE TERM
trap 'set +e; rm -rf "$tmp"' EXIT
trap 'rm -rf "$tmp"' HUP INT QUIT PIPE TERM
site="$tmp/site"
out="$tmp/out"

View File

@@ -4,4 +4,5 @@
set -euo pipefail
# HT_ADD_HTMLESCAPED* must reserve the escaper's worst case (6 for _full).
httrack -O /dev/null -#test=escape-room run | grep -q "escape-room self-test OK"
out=$(httrack -O /dev/null -#test=escape-room run)
grep -q "escape-room self-test OK" <<<"$out"

View File

@@ -8,7 +8,8 @@
set -euo pipefail
tmp=$(mktemp -d "${TMPDIR:-/tmp}/httrack_filelist.XXXXXX") || exit 1
trap 'rm -rf "$tmp"' EXIT HUP INT QUIT PIPE TERM
trap 'set +e; rm -rf "$tmp"' EXIT
trap 'rm -rf "$tmp"' HUP INT QUIT PIPE TERM
echo '<html><body>hi</body></html>' >"$tmp/index.html"

View File

@@ -18,7 +18,7 @@ MINGW* | MSYS* | CYGWIN*) exit 77 ;;
esac
dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT
trap 'set +e; rm -rf "$dir"' EXIT
# A few-hundred-char file path (kept well under the URL length limit) makes the
# {path} field long.
@@ -50,7 +50,7 @@ httrack "file://$deep/index.html" -O "$mir" -%F "$footer" -q -s0 -%v0 \
# The crawled page must exist (proves the URL wasn't rejected for length, so the
# footer path ran). Look under file/, not $mir, to skip the makeindex top index.
find "$mir/file" -name index.html | grep -q . || {
test -n "$(find "$mir/file" -name index.html)" || {
echo "page not mirrored; the oversized-footer path was not exercised" >&2
exit 1
}

View File

@@ -6,7 +6,7 @@
set -euo pipefail
dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT
trap 'set +e; rm -rf "$dir"' EXIT
rc=0
out=$(httrack -#test=fsize "$dir") || rc=$?

View File

@@ -4,4 +4,5 @@
set -euo pipefail
# get_ftp_line bounds a hostile CRLF-less FTP reply into its 1024-byte buffer.
httrack -O /dev/null -#test=ftp-line run | grep -q "ftp-line self-test OK"
out=$(httrack -O /dev/null -#test=ftp-line run)
grep -q "ftp-line self-test OK" <<<"$out"

View File

@@ -4,4 +4,5 @@
set -euo pipefail
# ftp_split_userpass bounds an over-long user:pass@ from a hostile ftp:// URL.
httrack -O /dev/null -#test=ftp-userpass run | grep -q "ftp-userpass self-test OK"
out=$(httrack -O /dev/null -#test=ftp-userpass run)
grep -q "ftp-userpass self-test OK" <<<"$out"

View File

@@ -10,7 +10,8 @@ echo "$out"
test "$out" == "growsize self-test OK"
tmp=$(mktemp -d "${TMPDIR:-/tmp}/httrack_growsize.XXXXXX")
trap 'rm -rf "$tmp"' EXIT HUP INT QUIT PIPE TERM
trap 'set +e; rm -rf "$tmp"' EXIT
trap 'rm -rf "$tmp"' HUP INT QUIT PIPE TERM
echo '<html><body>hi</body></html>' >"$tmp/index.html"
printf -- '-*/zzmarker*\n' >"$tmp/rules.txt"
@@ -18,7 +19,7 @@ printf -- '-*/zzmarker*\n' >"$tmp/rules.txt"
# the rules file lands in the URL/filter string, echoed back by the banner
run=$(httrack -O "$tmp/out" --quiet -n "-%S" "$tmp/rules.txt" \
"file://$tmp/index.html" 2>&1) || true
printf '%s\n' "$run" | grep -q 'zzmarker' || {
grep -q 'zzmarker' <<<"$run" || {
echo "FAIL: -%S rules file was not loaded"
printf '%s\n' "$run"
exit 1

View File

@@ -6,4 +6,4 @@ set -euo pipefail
# httrack internal hashtable autotest on 100K keys. Assert the success line (on
# stderr) so a misrouted registry entry can't pass on exit code alone.
out=$(httrack -#test=hashtable 100000 2>&1)
printf '%s\n' "$out" | grep -q "all hashtable tests were successful!" || exit 1
grep -q "all hashtable tests were successful!" <<<"$out" || exit 1

View File

@@ -4,4 +4,5 @@
set -euo pipefail
# inplace_escape_*() must match escape_*() on a copy: guards the shared helper.
httrack -O /dev/null -#test=inplace-escape run | grep -q "inplace-escape self-test OK"
out=$(httrack -O /dev/null -#test=inplace-escape run)
grep -q "inplace-escape self-test OK" <<<"$out"

View File

@@ -0,0 +1,8 @@
#!/bin/bash
#
# a log callback consumed the va_list the log file's vfprintf() still needed (#801).
set -euo pipefail
out=$(httrack -O /dev/null -#test=logcallback)
grep -q "logcallback self-test OK" <<<"$out"

View File

@@ -6,6 +6,7 @@ set -euo pipefail
# Drives -#test=longpath: a >MAX_PATH round trip exercising hts_pathToUCS2's
# \\?\ prefixing on Windows (#133); a positive control on POSIX.
dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT
trap 'set +e; rm -rf "$dir"' EXIT
httrack -O /dev/null -#test=longpath "$dir" | grep -q "longpath:.*OK"
out=$(httrack -O /dev/null -#test=longpath "$dir")
grep -q "longpath:.*OK" <<<"$out"

View File

@@ -6,7 +6,7 @@ set -euo pipefail
# hts_finish_makeindex writes the footer and gates the refresh meta on a single
# first link (guards the macro->function extraction).
dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT
trap 'set +e; rm -rf "$dir"' EXIT
httrack -O /dev/null -#test=makeindex "$dir" run |
grep -q "makeindex self-test OK"

View File

@@ -7,6 +7,7 @@ set -euo pipefail
# (>MAX_PATH) and non-ASCII, exercising the mirror I/O wrappers the engine's
# raw file ops now route to on Windows (#133, #630). Positive control on POSIX.
dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT
trap 'set +e; rm -rf "$dir"' EXIT
httrack -O /dev/null -#test=mirrorio "$dir" | grep -q "mirrorio:.*OK"
out=$(httrack -O /dev/null -#test=mirrorio "$dir")
grep -q "mirrorio:.*OK" <<<"$out"

View File

@@ -7,7 +7,8 @@
set -euo pipefail
tmp=$(mktemp -d "${TMPDIR:-/tmp}/httrack_parse.XXXXXX") || exit 1
trap 'rm -rf "$tmp"' EXIT HUP INT QUIT PIPE TERM
trap 'set +e; rm -rf "$tmp"' EXIT
trap 'rm -rf "$tmp"' HUP INT QUIT PIPE TERM
# a minimal valid 1x1 GIF, reused for every referenced asset
gif() {

View File

@@ -30,7 +30,8 @@ case "$bin" in
esac
tmp=$(mktemp -d "${TMPDIR:-/tmp}/httrack_rcfile.XXXXXX") || exit 1
trap 'rm -rf "$tmp"' EXIT HUP INT QUIT PIPE TERM
trap 'set +e; rm -rf "$tmp"' EXIT
trap 'rm -rf "$tmp"' HUP INT QUIT PIPE TERM
# HTS_HTTRACKRC is ".httrackrc" on POSIX but "httrackrc" on Windows: write both,
# each platform reads the one it knows.

View File

@@ -7,7 +7,7 @@
set -eu
dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT
trap 'set +e; rm -rf "$dir"' EXIT
out=$(httrack -#test=reconcile "$dir")

View File

@@ -6,4 +6,5 @@ set -euo pipefail
# #159: a redirect to a same-file alias (http<->https, user@host, ..) must be
# followed through, not turned into a self-pointing "moved" stub. The decision
# helper is exercised by the engine self-test.
httrack -O /dev/null -#test=redirect-samefile run | grep -q "redirect-samefile self-test OK"
out=$(httrack -O /dev/null -#test=redirect-samefile run)
grep -q "redirect-samefile self-test OK" <<<"$out"

View File

@@ -4,11 +4,11 @@
set -euo pipefail
# Drives -#test=renameover: hts_rename_over() must replace an existing dst, and
# must leave dst alone when the rename failed for a reason removing dst cannot
# fix (#779). The selftest prints the regime it detected; pin it per platform so
# a leg cannot pass having tested the other half.
# must leave dst alone when the rename failed for a reason moving dst aside
# cannot fix (#779, #790). The selftest prints the regime and the restore
# outcome it took; pin both per leg so none can pass having tested another.
dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT
trap 'set +e; rm -rf "$dir"' EXIT
case "$(uname -s)" in
MINGW* | MSYS_NT*) want=fallback ;; # native rename() refuses an existing target
@@ -16,8 +16,9 @@ MINGW* | MSYS_NT*) want=fallback ;; # native rename() refuses an existing target
esac
out=$(httrack -O /dev/null -#test=renameover "$dir")
echo "$out" | grep -q "renameover: OK"
echo "$out" | grep -q "renameover: regime $want"
grep -q "renameover: OK" <<<"$out"
grep -q "renameover: regime $want" <<<"$out"
grep -q "renameover: restore back" <<<"$out"
if [ "$(uname -s)" != "Linux" ]; then
echo "renameover: LD_PRELOAD interposition is Linux-only here, skipping"
@@ -42,14 +43,29 @@ fi
# refuses by default. The shim allocates nothing, so the ordering is harmless.
export ASAN_OPTIONS="${ASAN_OPTIONS:+$ASAN_OPTIONS:}verify_asan_link_order=0"
# The unlink fallback is dead code on POSIX, so borrow Windows' rename().
# The aside fallback is dead code on POSIX, so borrow Windows' rename().
out=$(LD_PRELOAD="$RENAMEFAIL_LIB" httrack -O /dev/null \
-#test=renameover "$dir")
echo "$out" | grep -q "renameover: OK"
echo "$out" | grep -q "renameover: regime fallback"
grep -q "renameover: OK" <<<"$out"
grep -q "renameover: regime fallback" <<<"$out"
grep -q "renameover: restore back" <<<"$out"
# #790: the move back out of the parked name fails once. Without the retry the
# old copy stays parked and dst is left absent.
out=$(RENAMEFAIL_ASIDE_FAILS=1 LD_PRELOAD="$RENAMEFAIL_LIB" httrack -O /dev/null \
-#test=renameover "$dir")
grep -q "renameover: OK" <<<"$out"
grep -q "renameover: restore back" <<<"$out"
# It keeps failing: the old copy must survive under the parked name, never be
# deleted, and the call must still report failure.
out=$(RENAMEFAIL_ASIDE_FAILS=9 LD_PRELOAD="$RENAMEFAIL_LIB" httrack -O /dev/null \
-#test=renameover "$dir")
grep -q "renameover: OK" <<<"$out"
grep -q "renameover: restore parked" <<<"$out"
# A source another process holds fails with EACCES, which dst had no part in.
out=$(RENAMEFAIL_MODE=locked LD_PRELOAD="$RENAMEFAIL_LIB" httrack -O /dev/null \
-#test=renameover "$dir")
echo "$out" | grep -q "renameover: OK"
echo "$out" | grep -q "renameover: regime refused"
grep -q "renameover: OK" <<<"$out"
grep -q "renameover: regime refused" <<<"$out"

View File

@@ -4,4 +4,5 @@
set -euo pipefail
# robots.txt RFC 9309 Allow/Disallow precedence (#452): longest match wins.
httrack -O /dev/null -#test=robots run | grep -q "robots self-test OK"
out=$(httrack -O /dev/null -#test=robots run)
grep -q "robots self-test OK" <<<"$out"

View File

@@ -12,7 +12,7 @@ httrack_bin=$(cd "$(dirname "$(command -v httrack)")" && pwd)/httrack
# scratch dir: body= and cached= write temp files (st-savename-body.tmp, hts-cache/)
scratch=$(mktemp -d)
trap 'rm -rf "$scratch"' EXIT
trap 'set +e; rm -rf "$scratch"' EXIT
cd "$scratch"
run() {
@@ -165,3 +165,20 @@ MINGW* | MSYS* | CYGWIN*)
name "/$(printf 'a%.0s' {1..300}).php" 'text/html' "$(printf 'a%.0s' {1..300}).html"
;;
esac
# #774: the engine owns hts-cache/ and hts-tmp/ inside the mirror, so a URL must
# never be able to name one; a site serving them gets the /nul -> nul_ treatment.
full '/hts-tmp/a.bin' 'application/octet-stream' \
'/dev/null/www.example.com/hts-tmp_/a.bin'
full '/hts-cache/new.zip' 'application/octet-stream' \
'/dev/null/www.example.com/hts-cache_/new.zip'
full '/d/hts-tmp/a.bin.bak' 'application/octet-stream' \
'/dev/null/www.example.com/d/hts-tmp_/a.bin.bak'
# A whole component only, and case-insensitively: the filesystem may be too.
full '/hts-tmpfoo/a.bin' 'application/octet-stream' \
'/dev/null/www.example.com/hts-tmpfoo/a.bin'
full '/HTS-TMP/a.bin' 'application/octet-stream' \
'/dev/null/www.example.com/HTS-TMP_/a.bin'
# Control: the DOS device names this reuses must still be escaped.
full '/nul/x.bin' 'application/octet-stream' \
'/dev/null/www.example.com/nul_/x.bin'

View File

@@ -7,11 +7,11 @@ set -eu
# Bare -#test lists known tests (printed to stderr).
list=$(httrack -#test 2>&1)
printf '%s\n' "$list" | grep -q "filter" || exit 1
printf '%s\n' "$list" | grep -q "cache-writefail" || exit 1
grep -q "filter" <<<"$list" || exit 1
grep -q "cache-writefail" <<<"$list" || exit 1
# Unknown name: non-zero exit + diagnostic, and no test result line.
rc=0
err=$(httrack -#test=bogus 2>&1) || rc=$?
test "$rc" -ne 0 || exit 1
printf '%s\n' "$err" | grep -q "Unknown self-test" || exit 1
grep -q "Unknown self-test" <<<"$err" || exit 1

View File

@@ -6,4 +6,5 @@ set -euo pipefail
# The SOCKS5 handshake framing and credential split, driven against scripted
# server replies (frame draining, oversize rejects, RFC 1929 fields).
httrack -O /dev/null '-#test=socks5' | grep -q "socks5 self-test OK"
out=$(httrack -O /dev/null '-#test=socks5')
grep -q "socks5 self-test OK" <<<"$out"

View File

@@ -4,4 +4,5 @@
set -euo pipefail
# HTTP status -> reason phrase, including the modern 429/451 (#453).
httrack -O /dev/null -#test=status run | grep -q "status self-test OK"
out=$(httrack -O /dev/null -#test=status run)
grep -q "status self-test OK" <<<"$out"

View File

@@ -5,4 +5,5 @@ set -euo pipefail
# --strip-query: pattern-scoped query-key stripping for dedup. All assertions
# live in the engine self-test (hts_query_strip_keys + fil_normalized_filtered).
httrack -O /dev/null -#test=stripquery | grep -q "strip-query self-test OK"
out=$(httrack -O /dev/null -#test=stripquery)
grep -q "strip-query self-test OK" <<<"$out"

View File

@@ -6,7 +6,7 @@
set -euo pipefail
dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT
trap 'set +e; rm -rf "$dir"' EXIT
httrack -O /dev/null -#test=structcheck "$dir" |
grep -q "structcheck self-test OK"

View File

@@ -4,7 +4,8 @@
set -euo pipefail
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/httrack_threadwait_st.XXXXXX") || exit 1
trap 'rm -rf "$tmpdir"' EXIT HUP INT QUIT PIPE TERM
trap 'set +e; rm -rf "$tmpdir"' EXIT
trap 'rm -rf "$tmpdir"' HUP INT QUIT PIPE TERM
# No pipe into grep: SIGPIPE would mask a failing exit status.
expect_ok() {

View File

@@ -6,7 +6,7 @@ set -euo pipefail
# hts_buildtopindex takes a system-charset path but verif_backblue below it
# expects utf-8, mangling a non-ASCII project dir on Windows (#216, #217).
dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT
trap 'set +e; rm -rf "$dir"' EXIT
httrack -O /dev/null -#test=topindex "$dir" run |
grep -q "topindex self-test OK"

View File

@@ -4,4 +4,5 @@
set -euo pipefail
# Entity/URL unescapers reserve one byte for the trailing NUL (no 1-byte OOB).
httrack -O /dev/null -#test=unescape-bounds run | grep -q "unescape-bounds self-test OK"
out=$(httrack -O /dev/null -#test=unescape-bounds run)
grep -q "unescape-bounds self-test OK" <<<"$out"

View File

@@ -5,4 +5,5 @@ set -euo pipefail
# -%u url-hack split (#271): www / // / query-order dedup toggle independently.
# All assertions live in the engine self-test (hash compare flag resolution).
httrack -O /dev/null -#test=urlhack run | grep -q "urlhack self-test OK"
out=$(httrack -O /dev/null -#test=urlhack run)
grep -q "urlhack self-test OK" <<<"$out"

View File

@@ -4,4 +4,5 @@
set -euo pipefail
# Default User-Agent (#449): honest HTTrack token, no Windows 98 relic.
httrack -O /dev/null -#test=useragent run | grep -q "useragent self-test OK"
out=$(httrack -O /dev/null -#test=useragent run)
grep -q "useragent self-test OK" <<<"$out"

View File

@@ -5,4 +5,5 @@ set -euo pipefail
# SURT canonicalization of the CDXJ sort key (--warc-cdx). Pure string work,
# so it runs under the MSan-instrumented 01_engine glob.
httrack -O /dev/null -#test=warc-surt | grep -q "warc-surt: OK"
out=$(httrack -O /dev/null -#test=warc-surt)
grep -q "warc-surt: OK" <<<"$out"

View File

@@ -5,7 +5,7 @@ set -euo pipefail
# Accept-Encoding (#450): advertise gzip+deflate; decode gzip/zlib/raw-deflate.
dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT
trap 'set +e; rm -rf "$dir"' EXIT
httrack -O /dev/null -#test=acceptencoding "$dir" run |
grep -q "acceptencoding self-test OK"

View File

@@ -7,7 +7,7 @@
set -eu
dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT
trap 'set +e; rm -rf "$dir"' EXIT
# the smashed-header case logs expected "Corrupted cache entry" warnings on
# stdout; the verdict is the last line

View File

@@ -27,7 +27,7 @@ test -e "$fixture/hts-cache/new.zip" || {
}
dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT
trap 'set +e; rm -rf "$dir"' EXIT
# Read against a private copy so the source tree is never touched (a read
# session does not write, but copying keeps the test hermetic). Create the dir

View File

@@ -6,7 +6,7 @@
set -eu
dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT
trap 'set +e; rm -rf "$dir"' EXIT
# the refusal errors land on stdout (no log file); pin them and the verdict
out=$(httrack -#test=cache-legacy "$dir" 2>/dev/null)

View File

@@ -1,8 +1,6 @@
#!/bin/bash
#
# Keep this POSIX-portable: the harness runs it via $(BASH), which is a plain
# POSIX /bin/sh on some platforms (e.g. macOS), so avoid bashisms and GNU-only
# tool flags despite the #!/bin/bash above.
# Stick to POSIX tool flags: macOS ships BSD grep/sed, not the GNU ones.
# Cache write-failure policy (-#test=cache-writefail <dir>). #174/#219: disk
# full or a failure streak aborts cleanly; an isolated failure or an oversized
@@ -11,19 +9,19 @@
set -eu
dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT
trap 'set +e; rm -rf "$dir"' EXIT
out=$(httrack -#test=cache-writefail "$dir")
# Match the exact success line (error logs also go to stdout); a renamed/removed
# test prints the registry to stderr, which exits non-zero but never prints this.
printf '%s\n' "$out" | grep -qx "cache-writefail: OK" || {
grep -qx "cache-writefail: OK" <<<"$out" || {
echo "expected 'cache-writefail: OK', got: $out" >&2
exit 1
}
# A skipped entry must be warned about with its URL.
printf '%s\n' "$out" | grep -q "entry not cached: example.com/" || {
grep -q "entry not cached: example.com/" <<<"$out" || {
echo "expected a URL-bearing skip warning" >&2
exit 1
}

View File

@@ -18,7 +18,7 @@
set -eu
dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT
trap 'set +e; rm -rf "$dir"' EXIT
# The working directory is a required argument; without it the test prints a
# usage line to stderr and returns non-zero.

View File

@@ -5,7 +5,7 @@ set -euo pipefail
# brotli/zstd decode, unknown codings, and the decoded-size budget.
dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT
trap 'set +e; rm -rf "$dir"' EXIT
httrack -O /dev/null -#test=contentcodings "$dir" run |
grep -q "contentcodings self-test OK"

View File

@@ -1,8 +1,6 @@
#!/bin/bash
#
# Keep this POSIX-portable: the harness runs it via $(BASH), which is a plain
# POSIX /bin/sh on some platforms (e.g. macOS), so avoid bashisms and GNU-only
# tool flags despite the #!/bin/bash above.
# Stick to POSIX tool flags: macOS ships BSD grep/sed, not the GNU ones.
# unzRepair header read must not overflow a signed shift (-#test=zip-repair-shift
# <dir>). A damaged local file header whose CRC high word has bit 15 set made
@@ -11,11 +9,11 @@
set -eu
dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT
trap 'set +e; rm -rf "$dir"' EXIT
out=$(httrack -#test=zip-repair-shift "$dir")
printf '%s\n' "$out" | grep -qx "zip-repair-shift: OK (recovered 1 entry)" || {
grep -qx "zip-repair-shift: OK (recovered 1 entry)" <<<"$out" || {
echo "expected 'zip-repair-shift: OK (recovered 1 entry)', got: $out" >&2
exit 1
}

View File

@@ -10,7 +10,7 @@ set -euo pipefail
httrack_bin=$(cd "$(dirname "$(command -v httrack)")" && pwd)/httrack
scratch=$(mktemp -d)
trap 'rm -rf "$scratch"' EXIT
trap 'set +e; rm -rf "$scratch"' EXIT
cd "$scratch"
name() {

View File

@@ -9,7 +9,7 @@ set -euo pipefail
httrack_bin=$(cd "$(dirname "$(command -v httrack)")" && pwd)/httrack
scratch=$(mktemp -d)
trap 'rm -rf "$scratch"' EXIT
trap 'set +e; rm -rf "$scratch"' EXIT
out=$("$httrack_bin" -O /dev/null -#test=warc-cdx "$scratch/")
echo "$out"

View File

@@ -10,13 +10,14 @@ set -euo pipefail
httrack_bin=$(cd "$(dirname "$(command -v httrack)")" && pwd)/httrack
if ! "$httrack_bin" -#test 2>&1 | grep -q '^ warc-wacz'; then
registry=$("$httrack_bin" -#test 2>&1 || true)
if ! grep -q '^ warc-wacz' <<<"$registry"; then
echo "warc-wacz self-test unavailable (build without OpenSSL); skipping"
exit 77
fi
scratch=$(mktemp -d)
trap 'rm -rf "$scratch"' EXIT
trap 'set +e; rm -rf "$scratch"' EXIT
out=$("$httrack_bin" -O /dev/null -#test=warc-wacz "$scratch/")
echo "$out"

View File

@@ -10,7 +10,7 @@ set -euo pipefail
httrack_bin=$(cd "$(dirname "$(command -v httrack)")" && pwd)/httrack
scratch=$(mktemp -d)
trap 'rm -rf "$scratch"' EXIT
trap 'set +e; rm -rf "$scratch"' EXIT
for t in warc warc-trunc warc-ftp warc-rotate warc-verbatim; do
out=$("$httrack_bin" -O /dev/null "-#test=$t" "$scratch/")

View File

@@ -29,7 +29,7 @@ command -v httrack >/dev/null 2>&1 || {
tmp=$(mktemp) || exit 1
committed_clean=$(mktemp) || exit 1
generated_clean=$(mktemp) || exit 1
trap 'rm -f "$tmp" "$committed_clean" "$generated_clean"' EXIT
trap 'set +e; rm -f "$tmp" "$committed_clean" "$generated_clean"' EXIT
README="$top_srcdir/README" bash "$gen" httrack >"$tmp" 2>/dev/null || {
echo "makeman.sh failed" >&2

View File

@@ -14,7 +14,7 @@ set -euo pipefail
site=$(mktemp -d)
out=$(mktemp -d)
trap 'rm -rf "$site" "$out"' EXIT
trap 'set +e; rm -rf "$site" "$out"' EXIT
cat >"$site/index.html" <<EOF
<a href="a.html">a</a> <a href="sub/b.html">b</a>

View File

@@ -30,7 +30,7 @@ test -n "$httrack_bin" || {
httrack_bin=$(cd "$(dirname "$httrack_bin")" && pwd)/$(basename "$httrack_bin")
work=$(mktemp -d)
trap 'kill -9 "${spid:-}" 2>/dev/null || true; rm -rf "$work"' EXIT
trap 'set +e; kill -9 "${spid:-}" 2>/dev/null || true; rm -rf "$work"' EXIT
mkdir -p "$work/root" "$work/proj"

View File

@@ -12,8 +12,8 @@ set -euo pipefail
: "${top_srcdir:=..}"
bash "$top_srcdir/tests/local-crawl.sh" \
--plant-file bigtrunc/slow.bin.bak \
--plant-dir bigtrunc/fast.bin.bak \
--plant-file bigtrunc/hts-tmp/slow.bin.bak \
--plant-dir bigtrunc/hts-tmp/fast.bin.bak \
--rerun-args '--update -M400000' \
--log-found 'More than 400000 bytes have been transferred.. giving up' \
--log-found 'replacing leftover backup .*slow\.bin\.bak' \

View File

@@ -25,7 +25,8 @@ cleanup() {
stop_server "$serverpid"
rm -rf "$tmpdir"
}
trap cleanup EXIT HUP INT QUIT PIPE TERM
trap 'set +e; cleanup' EXIT
trap cleanup HUP INT QUIT PIPE TERM
root="${tmpdir}/root"
out="${tmpdir}/crawl"
@@ -151,13 +152,15 @@ ok "the kept copies survived the update purge"
# --- the change report is the mirror's own account of the pass ----------------
test -s "$report" || fail "pass 2 wrote no ${report}"
test -z "$(listed gone)" || fail "the report calls something gone: $(listed gone)"
unchanged=$(listed unchanged)
changed=$(listed changed)
for name in keep empty; do
listed unchanged | grep -qx "${host}/${name}.bin:$(size_of "${tmpdir}/snap/${name}.bin")" ||
grep -qx "${host}/${name}.bin:$(size_of "${tmpdir}/snap/${name}.bin")" <<<"$unchanged" ||
fail "${name}.bin is not reported unchanged at its previous size"
if listed changed | grep -q "^${host}/${name}.bin:"; then
if grep -q "^${host}/${name}.bin:" <<<"$changed"; then
fail "${name}.bin is reported changed as well"
fi
done
listed changed | grep -qx "${host}/stay.bin:$(size_of "${out}/${host}/stay.bin")" ||
grep -qx "${host}/stay.bin:$(size_of "${out}/${host}/stay.bin")" <<<"$changed" ||
fail "stay.bin is not reported changed"
ok "the change report calls the kept files unchanged and the refreshed one changed"

View File

@@ -0,0 +1,81 @@
#!/bin/bash
#
# Issue #773: a test must fail only when an assertion fails. Under "set -e" a
# failing command in an EXIT trap becomes the script's exit status, so a
# teardown hiccup turned a test whose assertions all passed into a red CI leg.
set -euo pipefail
: "${top_srcdir:=..}"
tmp=$(mktemp -d)
trap 'set +e; rm -rf "$tmp"' EXIT
fail() {
echo "FAIL: $1" >&2
exit 1
}
# run <trap-body> <script-body>: exit status and stderr of a subject script.
run() {
{
echo "set -euo pipefail"
# printf, so the scanner below does not read this as a trap of our own
printf 'trap %s EXIT\n' "$1"
echo "$2"
} >"$tmp/subject"
rc=0
err=$(bash "$tmp/subject" 2>&1 >/dev/null) || rc=$?
}
# 1. the shape in the tree: teardown fails, every assertion passed -> pass
run "'set +e; false'" "true"
test "$rc" -eq 0 || fail "guarded teardown failure exited $rc, want 0"
# 2. teeth for case 1: unguarded, the same teardown is what went red
run "'false'" "true"
test "$rc" -eq 1 || fail "unguarded teardown failure exited $rc, want 1"
# 3. the guard must not swallow a real failure, nor a skip
run "'set +e; false'" "exit 1"
test "$rc" -eq 1 || fail "assertion failure exited $rc, want 1"
run "'set +e; false'" "exit 77"
test "$rc" -eq 77 || fail "skip exited $rc, want 77"
# 4. not failing must not mean going quiet: teardown still reports why
run "'set +e; rm /nonexistent-teardown-probe'" "true"
test "$rc" -eq 0 || fail "guarded teardown failure exited $rc, want 0"
case "$err" in
*nonexistent-teardown-probe*) ;;
*) fail "teardown error was silenced (stderr: '$err')" ;;
esac
echo "OK: teardown cannot decide the verdict, and still reports"
# 5. no test may reintroduce the shape case 2 pins. Two spellings neutralise
# errexit for teardown: "set +e" first, or "|| true" over the whole body.
scanned=0
for f in "$top_srcdir"/tests/*.test "$top_srcdir"/tests/*.sh; do
test -r "$f" || continue
scanned=$((scanned + 1))
done
# an unexpanded glob would report no offender having read nothing
test "$scanned" -ge 50 || fail "scanned $scanned scripts in $top_srcdir/tests, want the whole suite"
bad=$(awk '
FNR == 1 { errexit = 0 }
/^[[:space:]]*set[[:space:]]+(-[a-z]*e|-o[[:space:]]+errexit)/ { errexit = 1 }
!errexit { next }
!/^[[:space:]]*trap[[:space:]]+.*[[:space:]]EXIT([[:space:]]|;|$)/ { next }
{
guarded = /^[[:space:]]*trap[[:space:]]+.set \+e; / || /\|\|[[:space:]]+true.[[:space:]]+EXIT/
if (!guarded)
print FILENAME ":" FNR " unguarded"
# "set +e" on a trap shared with a signal leaves errexit off for the
# rest of the run, so a torn-down test could still report success
else if (/^[[:space:]]*trap[[:space:]]+.set \+e; / && /[[:space:]]EXIT[[:space:]]+[A-Z]/)
print FILENAME ":" FNR " set +e shared with a signal trap"
}
' "$top_srcdir"/tests/*.test "$top_srcdir"/tests/*.sh)
test -z "$bad" || fail "EXIT trap can decide the verdict:
$bad"
echo "OK: every EXIT trap under set -e runs teardown with errexit off"

View File

@@ -8,6 +8,6 @@ set -e
: "${top_srcdir:=..}"
tmp=$(mktemp -d "${TMPDIR:-/tmp}/httrack_longurl.XXXXXX")
trap 'rm -rf "$tmp"' EXIT
trap 'set +e; rm -rf "$tmp"' EXIT
httrack -O "$tmp/out" "-#test=warc-longurl" "$tmp"

View File

@@ -23,7 +23,8 @@ fail() {
}
tmp=$(mktemp -d "${TMPDIR:-/tmp}/httrack_sutmo.XXXXXX") || exit 1
trap 'rm -rf "$tmp"' EXIT HUP INT QUIT PIPE TERM
trap 'set +e; rm -rf "$tmp"' EXIT
trap 'rm -rf "$tmp"' HUP INT QUIT PIPE TERM
out="$tmp/out"
# --- a hanging test is killed, named, and reported 124 ----------------------

View File

@@ -0,0 +1,173 @@
#!/bin/bash
#
# #786: both cache-repair paths moved the recovered zip onto the cache without
# checking the move, and announced success either way. #824: they also committed
# a recovery holding no entry at all. Neither may replace the cache, and both
# must be reported.
set -euo pipefail
testdir=$(cd "$(dirname "$0")" && pwd)
# shellcheck source=tests/testlib.sh
. "${testdir}/testlib.sh"
tmpdir=$(mktemp -d) # honors the Windows-shaped TMPDIR the MSYS suite exports
cleanup() { rm -rf "$tmpdir"; }
trap 'set +e; cleanup' EXIT
trap 'set +e; cleanup; exit 1' HUP INT QUIT PIPE TERM
# One local file header, no central directory: unzOpen fails, unzRepair
# recovers the single entry. Same vector as -#test=zip-repair-shift.
plant_damaged_cache() {
rm -rf "$1"
mkdir -p "$1/hts-cache"
printf '\120\113\003\004\024\000\000\000\000\000\000\000\000\000\000\000\350\212\000\000\000\000\000\000\000\000\001\000\000\000\141' \
>"$1/hts-cache/new.zip"
}
dead=http://127.0.0.1:1/
# --- the CLI path (-#R), repair succeeding -----------------------------------
printf '[-#R repairs in place] ..\t'
proj="${tmpdir}/cli"
plant_damaged_cache "$proj"
out=$(httrack -O "$proj" -#R "$dead" 2>&1)
grep -q 'successfully recovered' <<<"$out" || {
echo "FAIL: no recovery reported: $out"
exit 1
}
test ! -e "${proj}/hts-cache/repair.zip" || {
echo "FAIL: the recovery was left in repair.zip"
exit 1
}
test "$(wc -c <"${proj}/hts-cache/new.zip")" -gt 31 || {
echo "FAIL: new.zip is still the damaged one"
exit 1
}
echo "OK"
# --- #824: nothing was recoverable, so nothing may be committed --------------
printf '[-#R refuses an empty recovery] ..\t'
proj="${tmpdir}/empty"
rm -rf "$proj"
mkdir -p "$proj/hts-cache"
printf 'not a zip at all, some bytes' >"$proj/hts-cache/new.zip"
rc=0
out=$(httrack -O "$proj" -#R "$dead" 2>&1) || rc=$?
test "$rc" -ne 0 || {
echo "FAIL: an empty recovery exited 0: $out"
exit 1
}
if grep -q 'successfully recovered' <<<"$out"; then
echo "FAIL: an empty recovery claimed success: $out"
exit 1
fi
test "$(wc -c <"${proj}/hts-cache/new.zip")" -eq 28 || {
echo "FAIL: the damaged cache was replaced by the empty recovery"
exit 1
}
echo "OK"
if [ "$(uname -s)" != "Linux" ]; then
echo "repair-rename: LD_PRELOAD interposition is Linux-only here, skipping"
exit 0
fi
# A --disable-shared build has nothing to preload; anything else missing is a
# build problem, not a skip, or the failure legs would pass vacuously.
if [ ! -r "${RENAMEFAIL_LA:-}" ]; then
echo "repair-rename: ${RENAMEFAIL_LA:-\$RENAMEFAIL_LA} was not built" >&2
exit 1
fi
if grep -q "^dlname=''" "$RENAMEFAIL_LA"; then
echo "repair-rename: static-only build, skipping the interposed legs"
exit 0
fi
if [ ! -r "${RENAMEFAIL_LIB:-}" ]; then
echo "repair-rename: ${RENAMEFAIL_LIB:-\$RENAMEFAIL_LIB} was not built" >&2
exit 1
fi
# An LD_PRELOAD library loads ahead of the executable's own libasan, which ASan
# refuses by default. The shim allocates nothing, so the ordering is harmless.
export ASAN_OPTIONS="${ASAN_OPTIONS:+$ASAN_OPTIONS:}verify_asan_link_order=0"
export RENAMEFAIL_MODE=repair
export LD_PRELOAD="$RENAMEFAIL_LIB"
# --- the CLI path, move refused ----------------------------------------------
printf '[-#R reports a refused move] ..\t'
proj="${tmpdir}/cli-fail"
plant_damaged_cache "$proj"
rc=0
out=$(httrack -O "$proj" -#R "$dead" 2>&1) || rc=$?
test "$rc" -ne 0 || {
echo "FAIL: a refused move exited 0: $out"
exit 1
}
if grep -q 'successfully recovered' <<<"$out"; then
echo "FAIL: a refused move still claimed success: $out"
exit 1
fi
test -s "${proj}/hts-cache/repair.zip" || {
echo "FAIL: the recovery was not kept for a retry"
exit 1
}
test "$(wc -c <"${proj}/hts-cache/new.zip")" -eq 31 || {
echo "FAIL: the damaged cache was destroyed anyway"
exit 1
}
echo "OK"
# --- the engine path (cache_init), move refused ------------------------------
printf '[cache_init reports a refused move] ..\t'
proj="${tmpdir}/engine"
plant_damaged_cache "$proj"
httrack -O "$proj" "$dead" --timeout=2 --retries=0 -q >/dev/null 2>&1 || true
log="${proj}/hts-log.txt"
test -r "$log" || {
echo "FAIL: no crawl log"
exit 1
}
grep -q 'damaged cache' "$log" || {
echo "FAIL: the repair path never ran"
exit 1
}
grep -q 'could not put the repaired cache in place' "$log" || {
echo "FAIL: a refused move was not reported: $(grep -i cache "$log")"
exit 1
}
if grep -q 'successfully recovered' "$log"; then
echo "FAIL: a refused move still claimed success"
exit 1
fi
test -s "${proj}/hts-cache/repair.zip" || {
echo "FAIL: the recovery was not kept for a retry"
exit 1
}
echo "OK"
# --- the move is refused the way Windows refuses it --------------------------
# The destination always exists there, so every repair takes hts_rename_over()'s
# fallback. Before #790 that fallback removed the cache and then failed.
printf '[a refused fallback keeps the cache] ..\t'
proj="${tmpdir}/eexist"
plant_damaged_cache "$proj"
rc=0
out=$(RENAMEFAIL_MODE=repair-eexist httrack -O "$proj" -#R "$dead" 2>&1) || rc=$?
test "$rc" -ne 0 || {
echo "FAIL: a refused fallback exited 0: $out"
exit 1
}
test "$(wc -c <"${proj}/hts-cache/new.zip")" -eq 31 || {
echo "FAIL: the fallback destroyed the cache"
exit 1
}
test -s "${proj}/hts-cache/repair.zip" || {
echo "FAIL: the recovery was not kept for a retry"
exit 1
}
leftover=$(find "${proj}/hts-cache" -name '*.hts-old*' | head -1)
test -z "$leftover" || {
echo "FAIL: the parked copy was left behind: $leftover"
exit 1
}
echo "OK"

View File

@@ -0,0 +1,21 @@
#!/bin/bash
#
# #774: the re-fetch backup lives in an hts-tmp directory beside the mirrored
# file, so the site here serves that exact path. url_savename must escape the
# reserved segment, landing the sibling in hts-tmp_ where the backup of a.bin
# cannot consume it. Asserting the unescaped path instead would pass with the
# bug live, which is how the first attempt at this test missed it.
set -euo pipefail
: "${top_srcdir:=..}"
bash "$top_srcdir/tests/local-crawl.sh" \
--rerun-args '--update' \
--file-matches 'bakname/a.bin' 'BAKNAME-MAIN-V2' \
--file-matches 'bakname/hts-tmp_/a.bin.bak' 'BAKNAME-SIBLING' \
--file-min-bytes 'bakname/hts-tmp_/a.bin.bak' 2048 \
--not-found 'bakname/hts-tmp/a.bin.bak' \
--log-not-found 'could not back up' \
--log-not-found 'could not restore' \
httrack 'BASEURL/bakname/index.html'

View File

@@ -0,0 +1,25 @@
#!/bin/bash
#
# Drives -#test=refetchbackup: the #77 re-fetch backup must leave a copy of the
# resource whatever happens, and must not build its temporary name inside the
# mirror namespace (#774, #775).
set -euo pipefail
dir=$(mktemp -d)
trap 'set +e; rm -rf "$dir"' EXIT
trap 'set +e; rm -rf "$dir"; exit 1' HUP INT QUIT PIPE TERM
rc=0
out=$(httrack -O /dev/null -#test=refetchbackup "$dir" 2>&1) || rc=$?
if test "$rc" -ne 0 || ! grep -q "refetchbackup: OK" <<<"$out"; then
echo "FAIL (exit $rc): $out" >&2
exit 1
fi
# The temporaries live in hts-tmp and the last user removes it.
leftovers=$(find "$dir" -mindepth 1 | head -5)
test -z "$leftovers" || {
echo "FAIL: leftover temporaries: $leftovers" >&2
exit 1
}

View File

@@ -0,0 +1,125 @@
#!/bin/bash
#
# FTP resumed with REST whenever the mirrored file merely existed, so an
# --update over a complete copy spliced the old body into the new one and the
# result matched the remote length (#798). Pass 1 is cut short to leave a real
# partial, pass 2 must still resume it, pass 3 must not.
set -euo pipefail
: "${top_srcdir:=..}"
testdir=$(cd "$(dirname "$0")" && pwd)
# shellcheck source=tests/testlib.sh
. "${testdir}/testlib.sh"
python=$(find_python) || ! echo "python3 not found; skipping" >&2 || exit 77
command -v httrack >/dev/null || {
echo "could not find httrack" >&2
exit 1
}
server=$(nativepath "${testdir}/ftp-server.py")
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/httrack_ftp.XXXXXX")
serverpid=
cleanup() {
stop_server "$serverpid"
rm -rf "$tmpdir"
}
trap 'set +e; cleanup' EXIT
trap cleanup HUP INT QUIT PIPE TERM
root="${tmpdir}/root"
out="${tmpdir}/crawl"
mode="${tmpdir}/mode"
cmds="${tmpdir}/cmds"
mkdir -p "$root" "$out"
fail() {
echo "FAIL: $*" >&2
exit 1
}
ok() { echo "OK: $*"; }
size_of() { wc -c <"$1" | tr -d '[:space:]'; }
# hts-log.txt is rewritten per pass, so this only ever sees the pass just run.
no_errors() {
local errors
errors=$(grep -a 'Error:' "${out}/hts-log.txt" || true)
test -z "$errors" || fail "$1 reported errors: ${errors}"
}
# Commands the server saw since the last pass; each pass starts by emptying it.
sent() { cat "$cmds"; }
# The two generations differ in length and in their first bytes, so a splice
# shows up whichever end of the file it lands in.
write_body() {
"$python" -c 'import sys; sys.stdout.buffer.write(
("%s-BODY " % sys.argv[1]).encode() + sys.argv[1][:1].encode() * int(sys.argv[2]))' \
"$1" "$2" >"${root}/a.bin"
}
write_body OLD 8000
echo '/a.bin truncate' >"$mode"
serverlog="${tmpdir}/server.out"
"$python" "$server" --root "$(nativepath "$root")" \
--mode-file "$(nativepath "$mode")" --log "$(nativepath "$cmds")" \
>"$serverlog" 2>&1 &
serverpid=$!
port=
for _ in $(seq 1 300); do
line=$(grep -m1 '^PORT ' "$serverlog" 2>/dev/null) && port="${line#PORT }" && break
kill -0 "$serverpid" 2>/dev/null || {
echo "ftp server exited early: $(cat "$serverlog")" >&2
exit 1
}
sleep 0.1
done
test -n "$port" || {
echo "could not discover ftp server port: $(cat "$serverlog")" >&2
exit 1
}
host="127.0.0.1_${port}"
url="ftp://127.0.0.1:${port}/a.bin"
mirror="${out}/${host}/a.bin"
# Bounded like every crawl pass: a wedge must fail the test, not hang the job.
crawl() { run_with_timeout 300 httrack "$url" -O "$out" --quiet \
--disable-security-limits --robots=0 --timeout=20 --max-time=120 --retries=1 \
-c1 "$@"; }
# --- pass 1: the transfer dies mid-body and leaves a partial -----------------
crawl >"${tmpdir}/log1" 2>&1 || true
test -f "$mirror" || fail "pass 1 mirrored nothing to resume"
partial=$(size_of "$mirror")
whole=$(size_of "${root}/a.bin")
if test "$partial" -le 0 || test "$partial" -ge "$whole"; then
fail "pass 1 left ${partial} bytes, wanted a partial of ${whole}"
fi
ok "pass 1 left a ${partial}-byte partial copy"
# --- pass 2: the partial is resumed ------------------------------------------
: >"$mode"
: >"$cmds"
crawl >"${tmpdir}/log2" 2>&1
case "$(sent)" in
*"REST ${partial}"*) ok "pass 2 resumed at the partial's ${partial} bytes" ;;
*) fail "pass 2 sent no REST ${partial}; commands were: $(sent)" ;;
esac
cmp -s "$mirror" "${root}/a.bin" ||
fail "the resumed copy is $(size_of "$mirror") bytes, served $(size_of "${root}/a.bin")"
# A resume used to count only the appended tail against the remote size, so it
# was reported "FTP file incomplete" even when it produced the whole file.
no_errors "pass 2"
ok "the resumed transfer produced the served body and no error"
# --- pass 3: --update over that complete copy --------------------------------
write_body NEW 12000
: >"$cmds"
crawl --update >"${tmpdir}/log3" 2>&1
case "$(sent)" in
*REST*) fail "the update resumed a complete mirror: $(sent)" ;;
*RETR*) ok "the update re-fetched without REST" ;;
*) fail "pass 3 never reached RETR; commands were: $(sent)" ;;
esac
cmp -s "$mirror" "${root}/a.bin" ||
fail "the updated copy is $(size_of "$mirror") bytes, served $(size_of "${root}/a.bin")"
no_errors "pass 3"
ok "the update replaced the mirror with the new body"

View File

@@ -31,7 +31,7 @@ cleanup() {
done
rm -rf "$tmpdir"
}
trap cleanup EXIT
trap 'set +e; cleanup' EXIT
# self-signed cert for the local TLS origin (httrack does not verify certs)
openssl req -x509 -newkey rsa:2048 -keyout "$tmpdir/key.pem" \

View File

@@ -40,9 +40,8 @@ serverpid=
cleanup() {
stop_server "$serverpid"
rm -rf "$tmpdir"
return 0
}
trap cleanup EXIT
trap 'set +e; cleanup' EXIT
# bind the live server to 127.0.0.1 only, so 127.0.0.2 refuses the connect
"$python" "$server" --root "$root" --bind 127.0.0.1 >"$tmpdir/srv.out" 2>"$tmpdir/srv.err" &

View File

@@ -13,7 +13,10 @@ bash "$top_srcdir/tests/local-crawl.sh" --errors 1 \
--not-found 'errpage/missing.html' \
httrack 'BASEURL/errpage/index.html' '-o0'
# Control -o1 (default): the 404 error page is written.
# Control -o1 (default): the 404 body is the server's verbatim, no HTTrack marker (#787); good.html proves the marker check can fire.
bash "$top_srcdir/tests/local-crawl.sh" --errors 1 \
--found 'errpage/missing.html' \
--file-matches 'errpage/missing.html' '404 error body' \
--file-not-matches 'errpage/missing.html' 'HTTrack' \
--file-matches 'errpage/good.html' 'HTTrack' \
httrack 'BASEURL/errpage/index.html' '-o1'

View File

@@ -24,7 +24,8 @@ cleanup() {
stop_server "$serverpid"
rm -rf "$tmpdir"
}
trap cleanup EXIT HUP INT QUIT PIPE TERM
trap 'set +e; cleanup' EXIT
trap cleanup HUP INT QUIT PIPE TERM
# OVERLAP_COUNTER gets a byte per flaky.bin request so pass 1 knows when to interrupt.
serverlog="${tmpdir}/server.log"

View File

@@ -6,7 +6,7 @@ set -euo pipefail
# --why: report which +/- filter rule decides for a URL, without crawling.
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
trap 'set +e; rm -rf "$tmpdir"' EXIT
why() {
local want="$1" out

View File

@@ -34,7 +34,7 @@ cleanup() {
done
rm -rf "$tmpdir"
}
trap cleanup EXIT
trap 'set +e; cleanup' EXIT
openssl req -x509 -newkey rsa:2048 -keyout "$tmpdir/key.pem" \
-out "$tmpdir/cert.pem" -days 2 -nodes -subj "/CN=127.0.0.1" \

View File

@@ -5,7 +5,7 @@
set -euo pipefail
dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT
trap 'set +e; rm -rf "$dir"' EXIT
printf 'HTTP/1.1 404 Not Found Here At All\r\nContent-Type: text/html\r\nLast-Modified: Wed, 01 Jan 2025 00:00:00 GMT\r\nContent-Length: 5\r\n\r\n' >"$dir/hdr"
printf 'hello' >"$dir/body"

View File

@@ -5,7 +5,7 @@
set -euo pipefail
dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT
trap 'set +e; rm -rf "$dir"' EXIT
# The first length-prefixed field declares far more than firstline[256] holds;
# cache_brstr must clamp the copy to the destination, not the declared length.

View File

@@ -29,9 +29,8 @@ serverpid=
cleanup() {
stop_server "$serverpid"
rm -rf "$tmpdir"
return 0
}
trap cleanup EXIT
trap 'set +e; cleanup' EXIT
# decoy origin: it must stay silent. LOCAL_SERVER_VERBOSE logs any request it gets.
LOCAL_SERVER_VERBOSE=1 "$python" "$server" --root "$root" --bind 127.0.0.1 \

View File

@@ -29,7 +29,7 @@ cleanup() {
done
rm -rf "$tmpdir"
}
trap cleanup EXIT
trap 'set +e; cleanup' EXIT
# start_server <logdir> <mode>: launches a proxy+origin pair, sets $origin_port
# and $proxy_port from its announced ephemeral ports.

View File

@@ -12,7 +12,7 @@ testdir=$(cd "$(dirname "$0")" && pwd)
. "${testdir}/testlib.sh"
tmp=$(mktemp -d "${TMPDIR:-/tmp}/httrack_wd.XXXXXX")
trap 'rm -rf "$tmp"' EXIT
trap 'set +e; rm -rf "$tmp"' EXIT
fail() {
echo "FAIL: $*" >&2
exit 1

View File

@@ -27,9 +27,8 @@ serverpid=
cleanup() {
stop_server "$serverpid"
rm -rf "$tmpdir"
return 0
}
trap cleanup EXIT
trap 'set +e; cleanup' EXIT
# start_stall_server <tag> <mode-args...>: sets $port from the announced one.
start_stall_server() {

View File

@@ -12,7 +12,7 @@ testdir=$(cd "$(dirname "$0")" && pwd)
TMPDIR=$(mktemp -d "${TMPDIR:-/tmp}/httrack_salv.XXXXXX")
export TMPDIR
trap 'rm -rf "$TMPDIR"' EXIT
trap 'set +e; rm -rf "$TMPDIR"' EXIT
fail() {
echo "FAIL: $*" >&2
exit 1

View File

@@ -23,7 +23,7 @@ funcs=$(sed -n '/^function lang_index/,/^}/p' "${script}")
# would truncate the lift and silently stop testing everything below it.
grep -q '^# Find the browser' "${script}" || fail "locale block terminator moved in ${script}"
block=$(sed -n '/^# Locale/,/^# Find the browser/p' "${script}" | sed '$d')
echo "${block}" | grep -q 'LANGN=.*lang_index' || fail "could not lift the whole locale block from ${script}"
grep -q 'LANGN=.*lang_index' <<<"${block}" || fail "could not lift the whole locale block from ${script}"
# Run the lifted block against whatever locale vars the caller exported.
run_block() {

View File

@@ -19,7 +19,7 @@ for f in "$eng" "$def"; do
done
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
trap 'set +e; rm -rf "$tmp"' EXIT
keys="$tmp/english.keys"
# Byte-wise: each file is in its own declared legacy charset, lang.def is CRLF.

View File

@@ -25,10 +25,11 @@ srv=
cleanup() {
test -z "${srv}" || kill -9 "${srv}" 2>/dev/null || true
}
trap cleanup EXIT HUP INT QUIT PIPE TERM
trap 'set +e; cleanup' EXIT
trap cleanup HUP INT QUIT PIPE TERM
log=$(mktemp)
trap 'cleanup; rm -f "${log}"' EXIT
trap 'set +e; cleanup; rm -f "${log}"' EXIT
# Ask htsserver for its rendered default base path, under the given $HOME
# ("-" = leave $HOME unset).

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