Compare commits

...

14 Commits

Author SHA1 Message Date
Xavier Roche
1cfd022cf3 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>
2026-07-27 14:16:29 +02:00
Xavier Roche
f98a3d1e63 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>
2026-07-27 13:39:10 +02:00
Xavier Roche
83079b788d Teardown must not decide a test's verdict (#792)
Under `set -e` a failing command in an EXIT trap becomes the script's exit status, so a hiccup while tearing down fixtures fails a test whose assertions all passed. That is what turned `57_local-proxy-connect.test` red on the Windows x64 leg of #765: five OK lines, no FAIL, exit 1. Every EXIT trap in the suite now runs teardown with errexit off, and the signal traps keep their own `trap` line, since sharing `set +e` with HUP/INT/QUIT/PIPE/TERM would leave errexit off for the rest of a signalled run and let a torn-down test still report success.

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

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

Closes #773

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

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

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

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

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

Does not fix #795, only makes it diagnosable.

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

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

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

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

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

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

Closes #778

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

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

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

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

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

Closes #785

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

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

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

Closes #771

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

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

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

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

Closes #779

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

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

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

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

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

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

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

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

Closes #770
Closes #768

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

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

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

Closes #769

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

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

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

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

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

Closes #758

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

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

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

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

Closes #759

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

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

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

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

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

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

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

---------

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

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

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

Closes #748

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

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

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

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

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

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

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

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

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

Closes #746

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

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

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

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

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

The message a cut connection produces depends on whether any bytes arrived, so
matching it would fail on a runner that sees none.

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 10:03:31 +02:00
122 changed files with 2256 additions and 474 deletions

View File

@@ -79,7 +79,10 @@ jobs:
- name: Build
run: make -j"$(nproc)"
# A backstop only: tests/test-timeout.sh bounds each test, and a healthy run
# is a minute here, two on macOS. Without it a stall ran to the job's 6h default.
- name: Test
timeout-minutes: 20
run: |
jobs=$(( $(nproc) * 2 )); [ "$jobs" -le 16 ] || jobs=16
make check -j"$jobs"
@@ -119,6 +122,7 @@ jobs:
run: make -j"$(nproc)"
- name: Test without python3
timeout-minutes: 20
run: |
set -euo pipefail
# Hide every python3* so `command -v python3` fails like it does in the
@@ -173,6 +177,7 @@ jobs:
sudo ifconfig lo0 alias 127.0.0.3 up
- name: Test
timeout-minutes: 20
run: |
jobs=$(( $(sysctl -n hw.ncpu) * 2 )); [ "$jobs" -le 16 ] || jobs=16
make check -j"$jobs"
@@ -245,6 +250,7 @@ jobs:
run: make -j"$(nproc)"
- name: Test
timeout-minutes: 20
run: |
jobs=$(( $(nproc) * 2 )); [ "$jobs" -le 16 ] || jobs=16
make check -j"$jobs"
@@ -302,6 +308,7 @@ jobs:
env:
ASAN_OPTIONS: detect_leaks=0:abort_on_error=1:halt_on_error=1:strict_string_checks=1:malloc_fill_byte=202:max_malloc_fill_size=2147483647:free_fill_byte=203:max_free_fill_size=2147483647
UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1
timeout-minutes: 20
run: |
jobs=$(( $(nproc) * 2 )); [ "$jobs" -le 16 ] || jobs=16
make check -j"$jobs"
@@ -345,6 +352,7 @@ jobs:
- name: Test (offline self-tests under MSan)
env:
MSAN_OPTIONS: abort_on_error=1:halt_on_error=1
timeout-minutes: 20
run: |
set -euo pipefail
# 01_engine-* only; zlib-dependent self-tests are named 01_zlib-* and
@@ -426,6 +434,7 @@ jobs:
run: make -j"$(nproc)"
- name: Test
timeout-minutes: 20
run: |
jobs=$(( $(nproc) * 2 )); [ "$jobs" -le 16 ] || jobs=16
make check -j"$jobs"

View File

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

View File

@@ -26,6 +26,10 @@ the operational checklist: toolchain, invariants, and how to ship a change.
check`, or `PATH="<bld>/src:$PATH"` for a manual run.
- Give new `.test` scripts `set -e`: the older ones predate the rule, so several
`local-crawl.sh` calls with no `set -e` report PASS on any non-last failure.
- Run teardown with errexit off: `trap 'set +e; cleanup' EXIT`. Under `set -e` a
failing cleanup command becomes the test's exit status (#773). Keep the other
signals on their own `trap` line, or errexit stays off for the rest of the run.
The guard also resets `$?`, so save it first if teardown reads it.
- Never 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

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

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

View File

@@ -340,12 +340,61 @@ static int back_index_ready(httrackp * opt, struct_back * sback, const char *adr
}
static int slot_can_be_cached_on_disk(const lien_back * back) {
/* A pending backup or spool means the slot is not finalized, and the swap
would unlink it through back_clear_entry() (#771). */
if (back->tmpfile != NULL && back->tmpfile[0] != '\0')
return 0;
return (back->status == STATUS_READY && back->locked == 0
&& back->url_sav[0] != '\0'
&& strcmp(back->url_sav, BACK_ADD_TEST) != 0);
/* Note: not checking !IS_DELAYED_EXT(back->url_sav) or it will quickly cause the slots to be filled! */
}
int back_selftest_slot_swap(void) {
lien_back back;
int err = 0;
#define CHECK(want, why) \
do { \
if (slot_can_be_cached_on_disk(&back) != (want)) { \
fprintf(stderr, "backswap: expected %d for %s\n", (want), (why)); \
err = 1; \
} \
} while (0)
memset(&back, 0, sizeof(back));
back.status = STATUS_READY;
strcpybuff(back.url_sav, "/tmp/httrack-selftest.bin");
CHECK(1, "a plain ready slot");
back.tmpfile = back.tmpfile_buffer;
strcpybuff(back.tmpfile_buffer, "/tmp/httrack-selftest.bin.bak");
CHECK(0, "a slot still holding a re-fetch backup");
/* Callers clear a spent temporary by emptying the name, not the pointer. */
back.tmpfile_buffer[0] = '\0';
CHECK(1, "a slot whose temporary was already dropped");
back.tmpfile = NULL;
back.locked = 1;
CHECK(0, "a locked slot");
back.locked = 0;
back.status = STATUS_TRANSFER;
CHECK(0, "a slot still transferring");
back.status = STATUS_READY;
back.url_sav[0] = '\0';
CHECK(0, "a slot with no save name");
strcpybuff(back.url_sav, BACK_ADD_TEST);
CHECK(0, "the dummy test slot");
#undef CHECK
printf("backswap self-test: %s\n", err ? "FAIL" : "OK");
return err;
}
/* Put all backing entries that are ready in the storage hashtable to spare space and CPU */
int back_cleanup_background(httrackp * opt, cache_back * cache,
struct_back * sback) {
@@ -585,6 +634,42 @@ static int create_back_tmpfile(httrackp *opt, lien_back *const back,
return 0;
}
/* Note: utf-8 */
void back_refetch_backup(httrackp *opt, lien_back *const back) {
back->tmpfile = NULL;
if (fexist_utf8(back->url_sav)) {
hts_boolean saved = HTS_FALSE;
if (create_back_tmpfile(opt, back, "bak") == 0) {
/* clobber a .bak a killed run left behind, or the guard stays off for
good (#758) */
if (fexist_utf8(back->tmpfile))
hts_log_print(opt, LOG_WARNING, "replacing leftover backup %s",
back->tmpfile);
saved = hts_rename_over(back->url_sav, back->tmpfile);
}
if (!saved) {
hts_log_print(opt, LOG_WARNING | LOG_ERRNO,
"could not back up %s; an aborted re-fetch will lose it",
back->url_sav);
back->tmpfile = NULL;
}
}
}
/* Did the fetch fail to produce a response, as opposed to the engine
deliberately passing the resource over? Only the latter may be purged. */
static hts_boolean back_transfer_failed(const int statuscode) {
switch (statuscode) {
case STATUSCODE_TOO_BIG:
case STATUSCODE_EXCLUDED:
case STATUSCODE_TEST_OK:
return HTS_FALSE;
default:
return statuscode <= 0 ? HTS_TRUE : HTS_FALSE;
}
}
/* Commit or restore a re-fetch backup (#77 follow-up): a re-fetch over an
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
@@ -1031,6 +1116,14 @@ int back_finalize(httrackp * opt, cache_back * cache, struct_back * sback,
/* Aborted, error, or not ready: url_sav (if written) is broken; restore the
previous copy from the backup. */
back_finalize_backup(opt, &back[p], HTS_FALSE);
/* Note the surviving copy, or the end-of-update purge drops what this run
never managed to replace (#746). */
if (!back[p].testmode && back_transfer_failed(back[p].r.statuscode) &&
back[p].url_sav[0] != '\0' && fexist_utf8(back[p].url_sav)) {
filenote(&opt->state.strc, back[p].url_sav, NULL);
file_notify(opt, back[p].url_adr, back[p].url_fil, back[p].url_sav, 0, 0,
back[p].r.notmodified);
}
return -1;
}
@@ -3143,20 +3236,7 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
back[i].url_sav, 1, 1,
back[i].r.notmodified);
back[i].r.compressed = 0;
/* Re-fetch over an existing file (#77 follow-up):
move the good copy aside before truncating it
so an aborted transfer can restore it. url_sav
is still written normally (file list intact).
*/
back[i].tmpfile = NULL;
if (fexist_utf8(back[i].url_sav)) {
if (create_back_tmpfile(opt, &back[i], "bak") !=
0 ||
RENAME(back[i].url_sav, back[i].tmpfile) !=
0) {
back[i].tmpfile = NULL;
}
}
back_refetch_backup(opt, &back[i]);
if ((back[i].r.out =
filecreate(&opt->state.strc,
back[i].url_sav)) == NULL) {

View File

@@ -139,6 +139,12 @@ int back_trylive(httrackp * opt, cache_back * cache, struct_back * sback,
const int p);
int back_finalize(httrackp * opt, cache_back * cache, struct_back * sback,
const int p);
/* Move the previous copy of back->url_sav to back->tmpfile so back_finalize()
can put it back when the re-fetch fails (#77 follow-up). Call right before
truncating url_sav; tmpfile stays NULL when there is nothing to save. */
void back_refetch_backup(httrackp *opt, lien_back *const back);
/* -#test=backswap: slots eligible for the on-disk ready table. */
int back_selftest_slot_swap(void);
void back_info(struct_back * sback, int i, int j, FILE * fp);
void back_infostr(struct_back *sback, int i, int j, char *s, size_t size);
LLint back_transferred(LLint add, struct_back * sback);

View File

@@ -855,13 +855,6 @@ static htsblk cache_readex_new(httrackp * opt, cache_back * cache,
return r;
}
// lecture d'un fichier dans le cache
// si save==null alors test unqiquement
static int hts_rename(httrackp * opt, const char *a, const char *b) {
hts_log_print(opt, LOG_DEBUG, "Cache: rename %s -> %s (%p %p)", a, b, a, b);
return RENAME(a, b);
}
/* Open the cache ZIP via hts_fopen_utf8 so a non-ASCII path_log isn't mangled
to ANSI (#630); 64-bit funcs keep multi-GB caches whole on Windows LLP64. */
static voidpf ZCALLBACK hts_zip_fopen_utf8(voidpf opaque, const void *filename,
@@ -991,29 +984,15 @@ void cache_init(cache_back * cache, httrackp * opt) {
OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
StringBuff(opt->path_log),
"hts-cache/new.zip")))) { // a previous cache exists.. rename it
/* Remove OLD cache */
if (fexist_utf8(fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
StringBuff(opt->path_log),
"hts-cache/old.zip"))) {
if (UNLINK(fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
StringBuff(opt->path_log), "hts-cache/old.zip")) !=
0) {
hts_log_print(opt, LOG_WARNING | LOG_ERRNO,
"Cache: error while moving previous cache");
}
}
/* Rename */
if (hts_rename
(opt,
fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt), StringBuff(opt->path_log),
"hts-cache/new.zip"), fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
StringBuff(opt->path_log),
"hts-cache/old.zip")) != 0) {
if (!hts_rename_over(
fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
StringBuff(opt->path_log), "hts-cache/new.zip"),
fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
StringBuff(opt->path_log), "hts-cache/old.zip"))) {
hts_log_print(opt, LOG_WARNING | LOG_ERRNO,
"Cache: error while moving previous cache");
} else {
hts_log_print(opt, LOG_DEBUG, "Cache: successfully renamed");
hts_log_print(opt, LOG_DEBUG, "Cache: rotated new.zip to old.zip");
}
}
} else {

View File

@@ -676,7 +676,7 @@ int cache_selftests(httrackp *opt, const char *dir) {
char base[HTS_URLMAXSIZE];
strcpybuff(base, dir);
if (base[0] != '\0' && base[strlen(base) - 1] != '/') {
if (base[0] != '\0' && hts_lastchar(base) != '/') {
strcatbuff(base, "/");
}
StringCopy(opt->path_log, base);
@@ -856,7 +856,7 @@ static void golden_setup(httrackp *opt, const char *dir) {
char base[HTS_URLMAXSIZE];
strcpybuff(base, dir);
if (base[0] != '\0' && base[strlen(base) - 1] != '/') {
if (base[0] != '\0' && hts_lastchar(base) != '/') {
strcatbuff(base, "/");
}
StringCopy(opt->path_log, base);

View File

@@ -773,7 +773,7 @@ int httpmirror(char *url1, httrackp * opt) {
// sauter les + sans rien après..
if (strnotempty(tempo)) {
if ((plus == 0) && (type == 1)) { // implicite: *www.edf.fr par exemple
if (tempo[strlen(tempo) - 1] != '*') {
if (hts_lastchar(tempo) != '*') {
strcatbuff(tempo, "*"); // ajouter un *
}
}
@@ -1819,60 +1819,6 @@ int httpmirror(char *url1, httrackp * opt) {
// -- -- --
// sauver fichier
/* En cas d'erreur, vérifier que fichier d'erreur existe */
if (strnotempty(savename()) == 0) { // chemin de sauvegarde existant
if (strcmp(urlfil(), "/robots.txt") == 0) { // pas robots.txt
if (store_errpage) { // c'est une page d'erreur
int create_html_warning = 0;
int create_gif_warning = 0;
switch (ishtml(opt, urlfil())) { /* pas fichier html */
case 0: /* non html */
{
char buff[256];
guess_httptype_sized(opt, buff, sizeof(buff), urlfil());
if (strcmp(buff, "image/gif") == 0)
create_gif_warning = 1;
}
break;
case 1: /* html */
if (!r.adr) {
}
break;
default: /* don't know.. */
break;
}
/* Créer message d'erreur ? */
if (create_html_warning) {
char *adr =
(char *) malloct(strlen(HTS_DATA_ERROR_HTML) + 1100);
hts_log_print(opt, LOG_INFO, "Creating HTML warning file (%s)",
r.msg);
if (adr) {
if (r.adr) {
freet(r.adr);
r.adr = NULL;
}
sprintf(adr, HTS_DATA_ERROR_HTML, r.msg);
r.adr = adr;
}
} else if (create_gif_warning) {
char *adr = (char *) malloct(HTS_DATA_UNKNOWN_GIF_LEN);
hts_log_print(opt, LOG_INFO, "Creating GIF dummy file (%s)",
r.msg);
if (r.adr) {
freet(r.adr);
r.adr = NULL;
}
memcpy(adr, HTS_DATA_UNKNOWN_GIF, HTS_DATA_UNKNOWN_GIF_LEN);
r.adr = adr;
}
}
}
}
if (strnotempty(savename()) == 0) { // pas de chemin de sauvegarde
if (strcmp(urlfil(), "/robots.txt") == 0) { // robots.txt
char BIGSTK sitemaps[8192];
@@ -2190,7 +2136,9 @@ int httpmirror(char *url1, httrackp * opt) {
continue;
strcpybuff(file, StringBuff(opt->path_html));
strcatbuff(file, line + 1);
file[strlen(file) - 1] = '\0';
/* strip filenote()'s ']', absent when linput() truncated the
line */
hts_striplastchar(file, ']');
hts_changes_previous(opt, file + StringLength(opt->path_html));
if (!strstr(adr, line)) { // not found in the new list?
if (fexist_utf8(file)) { // still on disk
@@ -2217,12 +2165,11 @@ int httpmirror(char *url1, httrackp * opt) {
fseek(old_lst, 0, SEEK_SET);
while(!feof(old_lst)) {
linput(old_lst, line, 1000);
while(strnotempty(line) && (line[strlen(line) - 1] != '/')
&& (line[strlen(line) - 1] != '\\')) {
line[strlen(line) - 1] = '\0';
while (strnotempty(line) && (hts_lastchar(line) != '/') &&
(hts_lastchar(line) != '\\')) {
hts_choplastchar(line);
}
if (strnotempty(line))
line[strlen(line) - 1] = '\0';
hts_choplastchar(line);
if (strnotempty(line))
if (!strstr(adr, line)) { // non trouvé?
char BIGSTK file[HTS_URLMAXSIZE * 2];
@@ -2235,13 +2182,12 @@ int httpmirror(char *url1, httrackp * opt) {
if (opt->log) {
hts_log_print(opt, LOG_INFO, "Purging directory %s/",
file);
while(strnotempty(file)
&& (file[strlen(file) - 1] != '/')
&& (file[strlen(file) - 1] != '\\')) {
file[strlen(file) - 1] = '\0';
while (strnotempty(file) &&
(hts_lastchar(file) != '/') &&
(hts_lastchar(file) != '\\')) {
hts_choplastchar(file);
}
if (strnotempty(file))
file[strlen(file) - 1] = '\0';
hts_choplastchar(file);
}
}
}

View File

@@ -376,8 +376,8 @@ void hts_finish_makeindex(httrackp *opt, int *makeindex_done,
const char *template_footer, const char *adr,
const char *fil);
// Flush ht_buff[0..ht_len] to save on disk (skip if MD5 unchanged); *fp
// closed+NULLed on write. Precondition: ht_len>0.
// Flush ht_buff[0..ht_len] to save on disk; *fp closed+NULLed on write.
// Precondition: ht_len>0.
void hts_finish_html_file(httrackp *opt, cache_back *cache, htsblk *r,
FILE **fp, const char *ht_buff, size_t ht_len,
const char *adr, const char *fil, const char *save);

View File

@@ -357,7 +357,7 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
char BIGSTK tempo[HTS_CDLMAXSIZE];
strcpybuff(tempo, argv[na] + 1);
if (tempo[0] == '\0' || tempo[strlen(tempo) - 1] != '"') {
if (hts_lastchar(tempo) != '"') {
char BIGSTK s[HTS_CDLMAXSIZE];
sprintf(s, "Missing quote in %s", argv[na]);
@@ -365,7 +365,7 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
htsmain_free();
return -1;
}
tempo[strlen(tempo) - 1] = '\0';
hts_choplastchar(tempo);
/* tempo is argv[na] minus its surrounding quotes, so it fits in place
*/
strlcpybuff(argv[na], tempo, strlen(argv[na]) + 1);
@@ -863,7 +863,7 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
char BIGSTK tempo[HTS_CDLMAXSIZE + 256];
strcpybuff(tempo, argv[na] + 1);
if (tempo[0] == '\0' || tempo[strlen(tempo) - 1] != '"') {
if (hts_lastchar(tempo) != '"') {
char s[HTS_CDLMAXSIZE + 256];
sprintf(s, "Missing quote in %s", argv[na]);
@@ -871,7 +871,7 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
htsmain_free();
return -1;
}
tempo[strlen(tempo) - 1] = '\0';
hts_choplastchar(tempo);
/* tempo is argv[na] minus its surrounding quotes, so it fits in place
*/
strlcpybuff(argv[na], tempo, strlen(argv[na]) + 1);
@@ -2724,10 +2724,7 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
char *a;
strcpybuff(rpath, StringBuff(opt->path_html));
if (rpath[0]) {
if (rpath[strlen(rpath) - 1] == '/')
rpath[strlen(rpath) - 1] = '\0';
}
hts_striplastchar(rpath, '/');
a = strrchr(rpath, '/');
if (a) {
*a = '\0';

View File

@@ -624,6 +624,9 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
} else {
file_notify(opt, back->url_adr, back->url_fil, back->url_sav, 1, 1,
0);
/* Every failure exit below would else leave the mirror truncated
(#771); the resume branch appends and needs no backup. */
back_refetch_backup(opt, back);
back->r.fp = filecreate(&opt->state.strc, back->url_sav);
}
strcpybuff(back->info, "receiving");

View File

@@ -83,9 +83,8 @@ void infomsg(const char *msg) {
/* try the flag as-is, then strip a trailing N as the numeric-arg
placeholder (cN -> c); this order keeps -%N from becoming -% */
p = optreal_find(cmd);
if (p < 0 && (int) strlen(cmd) > 2 &&
cmd[strlen(cmd) - 1] == 'N') {
cmd[strlen(cmd) - 1] = '\0';
if (p < 0 && (int) strlen(cmd) > 2 && hts_lastchar(cmd) == 'N') {
hts_striplastchar(cmd, 'N');
p = optreal_find(cmd);
}
if (p >= 0) {
@@ -211,7 +210,7 @@ void help_wizard(httrackp * opt) {
strcatbuff(str, "/websites/");
}
if (strnotempty(str))
if ((str[strlen(str) - 1] != '/') && (str[strlen(str) - 1] != '\\'))
if ((hts_lastchar(str) != '/') && (hts_lastchar(str) != '\\'))
strcatbuff(str, "/");
strcatbuff(stropt2, "-O \"");
strcatbuff(stropt2, str);
@@ -559,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

@@ -5078,7 +5078,7 @@ static int hts_dns_resolve_nocache_list(const char *const hostname,
if (!strnotempty(hostname) || max <= 0) {
return 0;
}
if ((hostname[0] == '[') && (hostname[strlen(hostname) - 1] == ']')) {
if ((hostname[0] == '[') && (hts_lastchar(hostname) == ']')) {
size_t size = strlen(hostname);
char *copy = malloct(size + 1);
int count;
@@ -5491,9 +5491,8 @@ void cut_path(char *fullpath, char *path, size_t path_size, char *pname,
size_t pname_size) {
path[0] = pname[0] = '\0';
if (strnotempty(fullpath)) {
if ((fullpath[strlen(fullpath) - 1] == '/')
|| (fullpath[strlen(fullpath) - 1] == '\\'))
fullpath[strlen(fullpath) - 1] = '\0';
if (!hts_striplastchar(fullpath, '/'))
hts_striplastchar(fullpath, '\\');
if (strlen(fullpath) > 1) {
char *a;
@@ -6648,9 +6647,12 @@ int hts_rename_utf8(const char *oldpath, const char *newpath) {
LPWSTR wnewpath = hts_pathToUCS2(newpath);
if (woldpath != NULL && wnewpath != NULL) {
const int result = _wrename(woldpath, wnewpath);
/* Save errno: callers key off it (#779) and free() may clobber it. */
const int err = errno;
free(woldpath);
free(wnewpath);
errno = err;
return result;
} else {
if (woldpath != NULL)

View File

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

View File

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

View File

@@ -539,6 +539,36 @@ static HTS_INLINE HTS_UNUSED HTS_PRINTF_FUN(3, 4) void slprintfbuff_clip(
#define sprintfbuff(ARR, ...) slprintfbuff((ARR), sizeof(ARR), __VA_ARGS__)
#endif
/* Last character of s, or '\0' when s is empty. Replaces s[strlen(s) - 1],
which indexes one byte before the buffer on an empty string. */
static HTS_INLINE HTS_UNUSED char hts_lastchar(const char *s) {
const size_t len = strlen(s);
return len != 0 ? s[len - 1] : '\0';
}
/* Drop a trailing c from s if present; HTS_TRUE if one was dropped. */
static HTS_INLINE HTS_UNUSED hts_boolean hts_striplastchar(char *s, char c) {
const size_t len = strlen(s);
if (len != 0 && s[len - 1] == c) {
s[len - 1] = '\0';
return HTS_TRUE;
}
return HTS_FALSE;
}
/* Drop the last character of s whatever it is; HTS_TRUE if s was not empty. */
static HTS_INLINE HTS_UNUSED hts_boolean hts_choplastchar(char *s) {
const size_t len = strlen(s);
if (len != 0) {
s[len - 1] = '\0';
return HTS_TRUE;
}
return HTS_FALSE;
}
/* Thin aliases over the libc allocator/memcpy (historical "t" suffix); no
added bounds checking. freet() also NULLs the freed pointer and tolerates
NULL. memcpybuff() despite the name is a raw memcpy: the caller owns the

View File

@@ -4944,6 +4944,101 @@ static int st_warc_surt(httrackp *opt, int argc, char **argv) {
return err;
}
/* A URL longer than the old 1024-byte header-format buffer must still reach the
archive: the record used to be abandoned whole, silently (#785). The sweep
straddles the boundary so both the stack-buffer and the grow path run. */
static int st_warc_longurl(httrackp *opt, int argc, char **argv) {
/* "WARC-Target-URI: " + CRLF costs 19 bytes, so the old buffer failed at
1005; 9000 forces several reallocs within one record. */
static const size_t lengths[] = {100, 1003, 1004, 1005, 1006, 2000, 9000};
static const char resp_hdr[] =
"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n";
char path[HTS_URLMAXSIZE * 2];
char body[64];
warc_writer *w;
FILE *fp;
char *blob;
LLint fsz;
const char *at2;
size_t i, n, nrec = 0;
int err = 0;
if (argc < 1) {
fprintf(stderr, "warc-longurl: need a writable directory\n");
return 1;
}
snprintf(path, sizeof(path), "%s/longurl.warc", argv[0]);
w = warc_open(opt, path);
if (w == NULL) {
fprintf(stderr, "warc-longurl: could not create %s\n", path);
return 1;
}
for (i = 0; i < sizeof(lengths) / sizeof(lengths[0]); i++) {
const size_t len = lengths[i];
char *uri = malloct(len + 1);
if (uri == NULL) {
warc_close(w);
return 1;
}
/* A distinct tail per URI so a truncated one cannot match another. */
snprintf(uri, len + 1, "http://example.com/%04d/", (int) len);
memset(uri + strlen(uri), 'a', len - strlen(uri));
uri[len] = '\0';
/* Distinct payloads: identical ones dedupe into revisit records. */
snprintf(body, sizeof(body), "<html><body>%04d</body></html>\n", (int) len);
if (warc_write_transaction(w, uri, NULL, NULL, resp_hdr, body, strlen(body),
NULL, 200, 0, 0) != 0) {
fprintf(stderr, "warc-longurl: write failed at length %d\n", (int) len);
err = 1;
}
freet(uri);
}
warc_close(w);
fsz = fsize_utf8(path);
blob = (fsz > 0) ? malloct((size_t) fsz + 1) : NULL;
if (blob == NULL) {
fprintf(stderr, "warc-longurl: no archive written\n");
return 1;
}
fp = FOPEN(path, "rb");
n = (fp != NULL) ? fread(blob, 1, (size_t) fsz, fp) : 0;
if (fp != NULL)
fclose(fp);
blob[n] = '\0';
for (i = 0; i < sizeof(lengths) / sizeof(lengths[0]); i++) {
const size_t len = lengths[i];
char want[64];
const char *at;
snprintf(want, sizeof(want), "WARC-Target-URI: http://example.com/%04d/",
(int) len);
at = strstr(blob, want);
if (at == NULL) {
fprintf(stderr, "warc-longurl: length %d lost its record\n", (int) len);
err = 1;
} else if (strlen(at) < strlen("WARC-Target-URI: ") + len ||
at[strlen("WARC-Target-URI: ") + len] != '\r') {
fprintf(stderr, "warc-longurl: length %d truncated\n", (int) len);
err = 1;
}
}
for (at2 = blob; (at2 = strstr(at2, "WARC-Type: response")) != NULL; at2++)
nrec++;
if (nrec != sizeof(lengths) / sizeof(lengths[0])) {
fprintf(stderr, "warc-longurl: %d response records, want %d\n", (int) nrec,
(int) (sizeof(lengths) / sizeof(lengths[0])));
err = 1;
}
freet(blob);
printf("warc-longurl: %s\n", err ? "FAIL" : "OK");
return err;
}
/* End-to-end CDXJ: crawl a handful of records with --warc-cdx, then verify the
.cdx is sorted, has exactly one line per response/revisit/resource (none for
warcinfo/request), and each offset/length points at a gzip member that
@@ -5955,6 +6050,104 @@ static int st_mirrorio(httrackp *opt, int argc, char **argv) {
return 0;
}
static void ro_put(const char *path, const char *data) {
FILE *const fp = FOPEN(path, "wb");
assertf(fp != NULL);
assertf(fwrite(data, 1, strlen(data), fp) == strlen(data));
fclose(fp);
}
/* HTS_TRUE if path holds exactly data. */
static hts_boolean ro_is(const char *path, const char *data) {
char buf[64];
FILE *const fp = FOPEN(path, "rb");
size_t n;
if (fp == NULL)
return HTS_FALSE;
n = fread(buf, 1, sizeof(buf), fp);
fclose(fp);
return n == strlen(data) && memcmp(buf, data, n) == 0 ? HTS_TRUE : HTS_FALSE;
}
// -#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
// 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;
}
char src[HTS_URLMAXSIZE * 2], dst[HTS_URLMAXSIZE * 2];
int err = 0;
fconcat(src, sizeof(src), argv[0], "renameover-src.bin");
fconcat(dst, sizeof(dst), argv[0], "renameover-dst.bin");
(void) UNLINK(src);
(void) UNLINK(dst);
ro_put(src, "probe");
ro_put(dst, "probe");
const int probe = RENAME(src, dst) == 0 ? 0 : errno;
/* Only a target in the way is something the unlink can clear. */
const hts_boolean replaceable = probe == 0 || probe == EEXIST;
printf("renameover: regime %s\n",
probe == 0 ? "clobber" : (probe == EEXIST ? "fallback" : "refused"));
(void) UNLINK(src);
(void) UNLINK(dst);
ro_put(src, "new");
ro_put(dst, "old");
if (replaceable) {
/* An existing dst must still be replaced: the unlink is for this. */
if (!hts_rename_over(src, dst)) {
fprintf(stderr, "renameover: replacing an existing dst failed: %s\n",
strerror(errno));
err++;
} else if (!ro_is(dst, "new") || fexist_utf8(src)) {
fprintf(stderr, "renameover: dst was not replaced by src\n");
err++;
}
} else {
/* A failure the unlink cannot fix must leave dst as it was. */
if (hts_rename_over(src, dst)) {
fprintf(stderr, "renameover: an unfixable failure reported success\n");
err++;
}
if (!ro_is(dst, "old")) {
fprintf(stderr, "renameover: an unfixable failure destroyed dst\n");
err++;
}
}
/* A missing src must leave dst alone and report failure. */
(void) UNLINK(src);
ro_put(dst, "keep");
if (hts_rename_over(src, dst)) {
fprintf(stderr, "renameover: a missing src reported success\n");
err++;
}
if (!ro_is(dst, "keep")) {
fprintf(stderr, "renameover: a missing src destroyed dst\n");
err++;
}
/* Same, with dst absent too: nothing to lose, still a failure. */
(void) UNLINK(dst);
if (hts_rename_over(src, dst)) {
fprintf(stderr, "renameover: a missing src and dst reported success\n");
err++;
}
(void) UNLINK(dst);
printf("renameover: %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) {
@@ -6268,6 +6461,13 @@ static void threadwait_gated_thread(void *arg) {
hts_mutexrelease(&threadwait_lock);
}
static int st_backswap(httrackp *opt, int argc, char **argv) {
(void) opt;
(void) argc;
(void) argv;
return back_selftest_slot_swap();
}
static int st_threadwait(httrackp *opt, int argc, char **argv) {
int err = 0;
int i, round;
@@ -6374,7 +6574,7 @@ static int st_changes_race(httrackp *opt, int argc, char **argv) {
return 1;
}
strcpybuff(base, argv[0]);
if (base[0] != '\0' && base[strlen(base) - 1] != '/')
if (base[0] != '\0' && hts_lastchar(base) != '/')
strcatbuff(base, "/");
StringCopy(opt->path_html, base);
StringCopy(opt->path_html_utf8, base);
@@ -6445,6 +6645,88 @@ static int st_changes_race(httrackp *opt, int argc, char **argv) {
return err;
}
/* The x[strlen(x) - 1] class (#770). The string starts mid-arena so the byte
it must not touch is a real neighbour; poisoned with '#', not 0, or a stray
NUL terminator would read as untouched. */
static int st_lastchar(httrackp *opt, int argc, char **argv) {
enum { off = 8 };
char arena[16];
char *const s = &arena[off];
const int guard = off - 1; /* what the old idiom clobbers */
int err = 0;
(void) opt;
(void) argc;
(void) argv;
#define REPOISON(str) \
do { \
memset(arena, '#', sizeof(arena)); \
strlcpybuff(s, (str), sizeof(arena) - off); \
} while (0)
#define CHECK(cond) \
do { \
if (!(cond)) { \
printf(" FAIL line %d: %s\n", __LINE__, #cond); \
err = 1; \
} \
} while (0)
/* the empty string: every helper must report "nothing" and touch nothing */
REPOISON("");
CHECK(hts_lastchar(s) == '\0');
CHECK(arena[guard] == '#');
REPOISON("");
CHECK(hts_striplastchar(s, '/') == HTS_FALSE);
CHECK(arena[guard] == '#');
CHECK(s[0] == '\0');
REPOISON("");
CHECK(hts_choplastchar(s) == HTS_FALSE);
CHECK(arena[guard] == '#');
CHECK(s[0] == '\0');
/* a '/' sitting where the underflow would land must not be mistaken for the
string's own last byte -- this is the #768 shape */
REPOISON("");
arena[guard] = '/';
CHECK(hts_lastchar(s) == '\0');
CHECK(hts_striplastchar(s, '/') == HTS_FALSE);
CHECK(arena[guard] == '/');
/* non-empty: ordinary behaviour */
REPOISON("ab/");
CHECK(hts_lastchar(s) == '/');
CHECK(hts_striplastchar(s, '/') == HTS_TRUE);
CHECK(strcmp(s, "ab") == 0);
CHECK(hts_striplastchar(s, '/') == HTS_FALSE);
CHECK(strcmp(s, "ab") == 0);
CHECK(hts_choplastchar(s) == HTS_TRUE);
CHECK(strcmp(s, "a") == 0);
CHECK(hts_choplastchar(s) == HTS_TRUE);
CHECK(s[0] == '\0');
CHECK(arena[guard] == '#');
/* one-character string: the boundary the guards get wrong */
REPOISON("/");
CHECK(hts_lastchar(s) == '/');
CHECK(hts_striplastchar(s, '/') == HTS_TRUE);
CHECK(s[0] == '\0');
CHECK(arena[guard] == '#');
/* control: the canary must be able to fail, or the checks above prove
nothing. Clobber it exactly as the unguarded idiom would. */
REPOISON("");
s[-1] = '\0';
CHECK(arena[guard] != '#');
#undef REPOISON
#undef CHECK
printf("lastchar self-test: %s\n", err ? "FAIL" : "OK");
return err;
}
/* ------------------------------------------------------------ */
/* Registry: name -> handler, with a usage hint and a one-line description. */
/* ------------------------------------------------------------ */
@@ -6500,12 +6782,16 @@ static const struct selftest_entry {
{"strsafe", "[overflow|overflow-buff|overflow-src [str]]",
"bounded string-op self-test", st_strsafe},
{"copyopt", "", "copy_htsopt option-copy self-test", st_copyopt},
{"lastchar", "", "last-char helpers never index before the buffer (#770)",
st_lastchar},
{"changes", "", "--changes bucket accounting and JSON escaping (#714)",
st_changes},
{"changes-race", "<dir>", "--changes under a late transfer thread (#714)",
st_changes_race},
{"threadwait", "", "htsthread_wait() joins threads spawned just before it",
st_threadwait},
{"backswap", "", "which backlog slots may be swapped to the ready table",
st_backswap},
{"pause", "", "randomized inter-file pause target self-test", st_pause},
{"relative", "<link> <curr-file>", "relative link between two paths",
st_relative},
@@ -6596,6 +6882,9 @@ static const struct selftest_entry {
st_warc_verbatim},
{"warc-surt", "", "SURT canonicalization of the CDXJ sort key",
st_warc_surt},
{"warc-longurl", "<dir>",
"a URL past the header-format buffer still reaches the archive",
st_warc_longurl},
{"longpath", "<dir>",
"round-trip a >MAX_PATH file through the _w* wrappers (\\\\?\\ on "
"Windows)",
@@ -6603,6 +6892,10 @@ static const struct selftest_entry {
{"mirrorio", "<dir>",
"round-trip a long+non-ASCII path through the mirror I/O wrappers",
st_mirrorio},
{"renameover", "<dir>",
"hts_rename_over(): replace dst, but never delete a dst it did not "
"replace",
st_renameover},
{"direnum", "<dir>",
"enumerate a long+non-ASCII directory through opendir/readdir",
st_direnum},

View File

@@ -1050,9 +1050,9 @@ int smallserver(T_SOC soc, char *url, char *method, char *data, char *path) {
if (!linput(fp, line, sizeof(line) - 2)) {
*str = '\0';
}
if (*str && str[strlen(str) - 1] == '\\') {
if (hts_lastchar(str) == '\\') {
nocr = 1;
str[strlen(str) - 1] = '\0';
hts_striplastchar(str, '\\');
}
while(*str) {
char *pos;
@@ -1169,11 +1169,8 @@ int smallserver(T_SOC soc, char *url, char *method, char *data, char *path) {
char *rpath = (char *) adr;
//find_handle h;
if (rpath[0]) {
if (rpath[strlen(rpath) - 1] == '/') {
rpath[strlen(rpath) - 1] = '\0'; /* note: patching stored (inhash) value */
}
}
/* note: patching stored (inhash) value */
hts_striplastchar(rpath, '/');
{
const char *profiles = hts_getcategories(rpath, 0);
const char *categ = hts_getcategories(rpath, 1);

View File

@@ -985,10 +985,7 @@ HTSEXT_API int hts_buildtopindex(httrackp * opt, const char *path,
&& toptemplate_bodycat) {
strcpybuff(rpath, path);
if (rpath[0]) {
if (rpath[strlen(rpath) - 1] == '/')
rpath[strlen(rpath) - 1] = '\0';
}
hts_striplastchar(rpath, '/');
fpo = fopen(fconcat(catbuff, sizeof(catbuff), rpath, "/index.html"), "wb");
if (fpo) {
@@ -1201,11 +1198,8 @@ HTSEXT_API char *hts_getcategories(char *path, int type) {
find_handle h;
coucal hashCateg = NULL;
if (rpath[0]) {
if (rpath[strlen(rpath) - 1] == '/') {
rpath[strlen(rpath) - 1] = '\0'; /* note: patching stored (inhash) value */
}
}
/* note: patching stored (inhash) value */
hts_striplastchar(rpath, '/');
h = hts_findfirst(rpath);
if (h) {
String iname = STRING_EMPTY;
@@ -1302,7 +1296,7 @@ HTSEXT_API find_handle hts_findfirst(char *path) {
strcpybuff(rpath, path);
if (rpath[0]) {
if (rpath[strlen(rpath) - 1] != '\\')
if (hts_lastchar(rpath) != '\\')
strcatbuff(rpath, "\\");
}
strcatbuff(rpath, "*.*");
@@ -1314,7 +1308,7 @@ HTSEXT_API find_handle hts_findfirst(char *path) {
strcpybuff(find->path, path);
{
if (find->path[0]) {
if (find->path[strlen(find->path) - 1] != '/')
if (hts_lastchar(find->path) != '/')
strcatbuff(find->path, "/");
}
}
@@ -1451,7 +1445,14 @@ hts_boolean hts_rename_over(const char *src, const char *dst) {
fconv(cdst, sizeof(cdst), dst);
if (RENAME(csrc, cdst) == 0)
return HTS_TRUE;
/* RENAME does not clobber an existing target on Windows. */
/* 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. */
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;
}

View File

@@ -138,7 +138,9 @@ 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. */
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);
#endif

View File

@@ -57,6 +57,9 @@ Please visit our Website: http://www.httrack.com
/* opt->state.warc value meaning "open failed once, do not retry". */
#define WARC_DISABLED ((void *) ~(uintptr_t) 0)
/* Suffix of the in-progress archive when a previous one must survive it. */
#define WARC_TMP_SUFFIX ".tmp"
struct warc_writer {
FILE *f;
httrackp *opt; /* kept for close-time logging (warc_wacz_package) */
@@ -86,6 +89,11 @@ struct warc_writer {
char *base_path; /* resolved archive path minus .warc[.gz] suffix */
const char *base_ext; /* ".warc.gz" or ".warc" (static) */
char *arc_path; /* full single-file archive path (NULL under rotation) */
/* A re-run must not destroy an archive it cannot replace (#759). */
hts_boolean protect_prev; /* previous archive present: build in a temp */
hts_boolean opened; /* open completed; a failed one swaps nothing */
hts_boolean failed; /* a record or segment was lost: swap nothing */
uint64_t unbacked_revisits; /* revisits whose payload no file here holds */
char **page_lines; /* one JSON page line per 200 text/html response, owned */
size_t page_count;
size_t page_cap;
@@ -119,24 +127,32 @@ static void wbuf_free(wbuf *b) {
b->len = b->cap = 0;
}
/* Append n bytes; returns 0 on success, -1 on OOM/overflow. */
static int wbuf_add(wbuf *b, const void *p, size_t n) {
/* Make room for n more bytes; returns 0 on success, -1 on OOM/overflow. */
static int wbuf_reserve(wbuf *b, size_t n) {
size_t ncap;
char *nd;
if (n > (size_t) -1 - b->len)
return -1;
if (b->len + n > b->cap) {
size_t ncap = b->cap ? b->cap : 256;
char *nd;
while (ncap < b->len + n) {
if (ncap > (size_t) -1 / 2)
return -1;
ncap *= 2;
}
nd = realloct(b->data, ncap);
if (nd == NULL)
if (b->len + n <= b->cap)
return 0;
ncap = b->cap ? b->cap : 256;
while (ncap < b->len + n) {
if (ncap > (size_t) -1 / 2)
return -1;
b->data = nd;
b->cap = ncap;
ncap *= 2;
}
nd = realloct(b->data, ncap);
if (nd == NULL)
return -1;
b->data = nd;
b->cap = ncap;
return 0;
}
/* Append n bytes; returns 0 on success, -1 on OOM/overflow. */
static int wbuf_add(wbuf *b, const void *p, size_t n) {
if (wbuf_reserve(b, n) != 0)
return -1;
memcpy(b->data + b->len, p, n);
b->len += n;
return 0;
@@ -148,16 +164,32 @@ static int wbuf_puts(wbuf *b, const char *s) {
static int wbuf_printf(wbuf *b, const char *fmt, ...) HTS_PRINTF_FUN(2, 3);
/* A header line carrying a URL has no useful bound, and giving up here loses
the whole record, so anything past the stack buffer is formatted into the
wbuf itself rather than rejected (#785). */
static int wbuf_printf(wbuf *b, const char *fmt, ...) {
char tmp[1024];
size_t need;
int n;
va_list ap;
va_start(ap, fmt);
n = vsnprintf(tmp, sizeof(tmp), fmt, ap);
va_end(ap);
if (n < 0 || (size_t) n >= sizeof(tmp))
if (n < 0)
return -1;
return wbuf_add(b, tmp, (size_t) n);
if ((size_t) n < sizeof(tmp))
return wbuf_add(b, tmp, (size_t) n);
need = (size_t) n + 1; /* +1: vsnprintf always writes the NUL */
if (wbuf_reserve(b, need) != 0)
return -1;
va_start(ap, fmt);
n = vsnprintf(b->data + b->len, need, fmt, ap);
va_end(ap);
/* Never advance past what was reserved, whatever the second pass returns. */
if (n < 0 || (size_t) n >= need)
return -1;
b->len += (size_t) n; /* the NUL is scratch, overwritten by the next append */
return 0;
}
/* ---- gzip-per-record member writer (mirrors ae_write_packed) ---- */
@@ -1129,13 +1161,17 @@ static int warc_emit(warc_writer *w, const char *type, const char *content_type,
never split a record, and never rotate a warcinfo (it opens a segment). */
if (w->max_size > 0 && w->seg_base != NULL && w->offset >= w->max_size &&
strcmp(type, "warcinfo") != 0) {
if (warc_rotate(w) != 0)
if (warc_rotate(w) != 0) {
w->failed = HTS_TRUE;
return -1;
}
}
/* F4: overflow-safe block length; http_hdr_len+sep is provably small. */
if (payload > (size_t) -1 - http_hdr_len - sep)
if (payload > (size_t) -1 - http_hdr_len - sep) {
w->failed = HTS_TRUE;
return -1;
}
block_len = http_hdr_len + sep + payload;
memset(&hdr, 0, sizeof(hdr));
@@ -1241,11 +1277,23 @@ static int warc_emit(warc_writer *w, const char *type, const char *content_type,
rc = 0;
done:
wbuf_free(&hdr);
if (rc != 0)
w->failed = HTS_TRUE; /* a truncated run must not replace a whole one */
return rc;
}
/* ---- segment rotation (--warc-max-size) ---- */
/* Path to open for the segment whose final path is `final`: that path itself,
or a sibling temp while a previous archive must survive until close. */
static const char *warc_open_path(warc_writer *w, const char *final, char *buf,
size_t bufsz) {
if (!w->protect_prev)
return final;
snprintf(buf, bufsz, "%s" WARC_TMP_SUFFIX, final);
return buf;
}
/* Emit the warcinfo that heads a segment; sets w->info_id for its records. */
static int warc_write_warcinfo_record(warc_writer *w) {
w->info_id[0] = '\0'; /* warcinfo itself carries no WARC-Warcinfo-ID */
@@ -1257,17 +1305,23 @@ static int warc_write_warcinfo_record(warc_writer *w) {
static int warc_rotate(warc_writer *w) {
char namebuf[HTS_URLMAXSIZE * 2];
char openbuf[HTS_URLMAXSIZE * 2 + sizeof(WARC_TMP_SUFFIX)];
char catbuff[CATBUFF_SIZE];
if (w->f != NULL) {
fclose(w->f);
w->f = NULL;
}
w->seg++;
snprintf(namebuf, sizeof(namebuf), "%s-%05u%s", w->seg_base, w->seg,
const unsigned next = w->seg + 1;
FILE *f;
snprintf(namebuf, sizeof(namebuf), "%s-%05u%s", w->seg_base, next,
w->seg_ext);
w->f = FOPEN(fconv(catbuff, sizeof(catbuff), namebuf), "wb");
if (w->f == NULL)
/* Open before advancing: w->seg must only ever name a segment that exists,
or close-time packaging and swapping would work on a missing file. */
f = FOPEN(fconv(catbuff, sizeof(catbuff),
warc_open_path(w, namebuf, openbuf, sizeof(openbuf))),
"wb");
if (f == NULL)
return -1;
if (w->f != NULL)
fclose(w->f);
w->f = f;
w->seg = next;
w->offset = 0;
if (w->cdx_on) {
freet(w->cur_seg);
@@ -1324,6 +1378,7 @@ void warc_adopt_rawspool(htsblk *r, const char *tmpfile_path) {
warc_writer *warc_open(httrackp *opt, const char *path) {
warc_writer *w;
char namebuf[HTS_URLMAXSIZE * 2];
char openbuf[HTS_URLMAXSIZE * 2 + sizeof(WARC_TMP_SUFFIX)];
char catbuff[CATBUFF_SIZE];
wbuf info;
const char *robots;
@@ -1474,35 +1529,105 @@ warc_writer *warc_open(httrackp *opt, const char *path) {
path = namebuf;
}
w->f = FOPEN(fconv(catbuff, sizeof(catbuff), path), "wb");
if (w->max_size == 0 && (w->arc_path = strdupt(path)) == NULL) {
warc_close(w);
return NULL;
}
/* Set only once arc_path is recorded, so a half-built writer never swaps. */
w->protect_prev = fsize_utf8(path) > 0 ? HTS_TRUE : HTS_FALSE;
w->f = FOPEN(fconv(catbuff, sizeof(catbuff),
warc_open_path(w, path, openbuf, sizeof(openbuf))),
"wb");
if (w->f == NULL) {
warc_close(w);
return NULL;
}
if (w->cdx_on)
w->cur_seg = path_basename_dup(path);
if (w->wacz_on && w->max_size == 0)
w->arc_path = strdupt(path); /* single-file: package this exact path */
if (warc_write_warcinfo_record(w) != 0) {
warc_close(w);
return NULL;
}
w->opened = HTS_TRUE;
return w;
}
/* Final path of segment s (the run's only archive when rotation is off). */
static const char *warc_seg_path(warc_writer *w, unsigned s, char *buf,
size_t bufsz) {
if (w->max_size == 0)
return w->arc_path;
snprintf(buf, bufsz, "%s-%05u%s", w->seg_base, s, w->seg_ext);
return buf;
}
/* Swap this run's archive into place, unless it only holds revisits naming
bodies the previous archive still has and this one does not (#759).
HTS_FALSE: the previous archive was kept, so leave its .cdx and .wacz too. */
static hts_boolean warc_commit(warc_writer *w) {
char finalbuf[HTS_URLMAXSIZE * 2];
char tmpbuf[HTS_URLMAXSIZE * 2 + sizeof(WARC_TMP_SUFFIX)];
char catbuff[CATBUFF_SIZE];
const unsigned nseg = (w->max_size > 0) ? w->seg + 1 : 1;
hts_boolean swap;
unsigned s;
if (!w->protect_prev)
return HTS_TRUE; /* nothing was there to lose: written in place */
/* All or nothing: a swap stopping halfway would mix this run's segments with
the previous one's, so require every temp before renaming any. */
swap = w->opened && !w->failed && w->unbacked_revisits == 0;
for (s = 0; s < nseg && swap; s++) {
snprintf(tmpbuf, sizeof(tmpbuf), "%s" WARC_TMP_SUFFIX,
warc_seg_path(w, s, finalbuf, sizeof(finalbuf)));
swap = fsize_utf8(tmpbuf) > 0;
}
if (swap) {
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)) {
hts_log_print(w->opt, LOG_ERROR | LOG_ERRNO,
"WARC: could not replace %s", final);
return HTS_FALSE;
}
}
return HTS_TRUE;
}
for (s = 0; s < nseg; s++) {
snprintf(tmpbuf, sizeof(tmpbuf), "%s" WARC_TMP_SUFFIX,
warc_seg_path(w, s, finalbuf, sizeof(finalbuf)));
(void) UNLINK(fconv(catbuff, sizeof(catbuff), tmpbuf));
}
if (w->unbacked_revisits > 0)
hts_log_print(
w->opt, LOG_ERROR,
"WARC: this pass revisited %llu URL(s) without re-downloading them, so "
"its archive would name bodies no file holds; kept the previous %s "
"(re-run with -C0, or --warc-file with a name of its own)",
(unsigned long long) w->unbacked_revisits,
warc_seg_path(w, 0, finalbuf, sizeof(finalbuf)));
return HTS_FALSE;
}
void warc_close(warc_writer *w) {
size_t i;
if (w == NULL)
return;
warc_cdx_flush(w); /* sort + write <base>.cdx before tearing down */
if (w->f != NULL)
fclose(w->f);
w->f = NULL;
if (warc_commit(w)) {
warc_cdx_flush(w); /* sort + write <base>.cdx beside the archive */
#if HTS_USEOPENSSL
if (w->wacz_on) /* package once the segment(s) + .cdx are closed on disk */
warc_wacz_package(w);
if (w->wacz_on) /* package once the segment(s) + .cdx are closed on disk */
warc_wacz_package(w);
#endif
}
if (w->seen != NULL)
coucal_delete(&w->seen);
for (i = 0; i < w->cdx_count; i++)
@@ -1583,6 +1708,12 @@ int warc_write_transaction(warc_writer *w, const char *target_uri,
if (is_update_unchanged) {
is_revisit = 1;
profile = "http://netpreserve.org/warc/1.1/revisit/server-not-modified";
/* Replay resolves a revisit by this field alone, and a 304 stands in for
the same URL. No WARC-Refers-To-Date to go with it: the cache keeps the
document's Last-Modified, never the previous capture time. */
refers_uri = target_uri;
/* Served from cache: the payload sits in the previous archive, not here. */
w->unbacked_revisits++;
} else if (have_pdig && w->seen != NULL) {
void *prev = NULL;
if (coucal_read_pvoid(w->seen, pdig, &prev) && prev != NULL) {

View File

@@ -0,0 +1,8 @@
#!/bin/bash
#
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"

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

@@ -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,6 @@ 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"

View File

@@ -6,6 +6,6 @@ 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"

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

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

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

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

View File

@@ -0,0 +1,35 @@
#!/bin/bash
#
# Runs the lastchar self-test (#770), then keeps the idiom it replaced from
# coming back: x[strlen(x) - 1] indexes one byte before the buffer when x is
# empty, and the guard is never where the index is.
set -eu
: "${top_srcdir:=..}"
out=$(httrack -#test=lastchar) || {
echo "httrack -#test=lastchar exited non-zero: $out" >&2
exit 1
}
# Exact-match the success line so a renamed/removed test can't pass silently.
test "$out" = "lastchar self-test: OK" || {
echo "expected 'lastchar self-test: OK', got: $out" >&2
exit 1
}
# The self-test only covers the helpers, so guard the class at the source: the
# lone survivor is inside a comment block in htsname.c. coucal is vendored, so
# its idioms are not ours to police.
hits=$(command grep -rn 'strlen([^)]*) *- *1\]' "$top_srcdir/src" \
--include='*.c' --include='*.h' --exclude-dir=coucal |
grep -v 'htsname\.c:.*save\[strlen(save)-1\]' |
grep -v 'Replaces s\[strlen(s) - 1\]' |
grep -v 'The x\[strlen(x) - 1\] class' || true)
test -z "$hits" || {
echo "x[strlen(x) - 1] reintroduced; use hts_lastchar/hts_striplastchar/hts_choplastchar:" >&2
echo "$hits" >&2
exit 1
}

View File

@@ -6,6 +6,6 @@ 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"

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,6 @@ 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"

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

@@ -0,0 +1,55 @@
#!/bin/bash
#
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.
dir=$(mktemp -d)
trap 'set +e; rm -rf "$dir"' EXIT
case "$(uname -s)" in
MINGW* | MSYS_NT*) want=fallback ;; # native rename() refuses an existing target
*) want=clobber ;;
esac
out=$(httrack -O /dev/null -#test=renameover "$dir")
echo "$out" | grep -q "renameover: OK"
echo "$out" | grep -q "renameover: regime $want"
if [ "$(uname -s)" != "Linux" ]; then
echo "renameover: 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 interposed legs would pass vacuously.
if [ ! -r "${RENAMEFAIL_LA:-}" ]; then
echo "renameover: ${RENAMEFAIL_LA:-\$RENAMEFAIL_LA} was not built" >&2
exit 1
fi
if grep -q "^dlname=''" "$RENAMEFAIL_LA"; then
echo "renameover: static-only build, skipping the interposed legs"
exit 0
fi
if [ ! -r "${RENAMEFAIL_LIB:-}" ]; then
echo "renameover: ${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"
# The unlink 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"
# 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"

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() {

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

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

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

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

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

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

@@ -16,7 +16,7 @@ if ! "$httrack_bin" -#test 2>&1 | grep -q '^ warc-wacz'; then
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

@@ -0,0 +1,111 @@
#!/bin/bash
#
# The end-of-mirror purge read old.lst with linput(fp, line, 1000), which caps a
# chunk at 999 bytes. filenote() writes "[<save path>]", so a 998-byte save path
# makes a 1000-byte line that comes back as 999 bytes plus a one-byte "]" tail.
# For that tail `line + 1` is empty, and with no -O so is path_html, so the purge
# indexed file[-1] (#770). Needs no -O, hence not local-crawl.sh.
#
# The OOB write lands in stack padding, so only the sanitized CI leg fails on
# unfixed code; the length assertion below is what keeps this from passing
# vacuously if the save path ever stops landing on the chunk boundary.
set -euo pipefail
: "${top_srcdir:=..}"
# shellcheck source=tests/testlib.sh
. "$top_srcdir/tests/testlib.sh"
# python3 is required; mirror check-network.sh's skip-with-77 convention.
python=$(find_python) || ! echo "python3 not found; skipping" >&2 || exit 77
# Resolve httrack now: the harness prepends a RELATIVE dir to PATH, so the cd
# below would otherwise silently fall through to an installed copy.
httrack_bin=$(command -v httrack)
test -n "$httrack_bin" || {
echo "httrack not on PATH" >&2
exit 1
}
httrack_bin=$(cd "$(dirname "$httrack_bin")" && pwd)/$(basename "$httrack_bin")
work=$(mktemp -d)
trap 'set +e; kill -9 "${spid:-}" 2>/dev/null || true; rm -rf "$work"' EXIT
mkdir -p "$work/root" "$work/proj"
"$python" "$top_srcdir/tests/local-server.py" --root "$work/root" \
>"$work/server.out" 2>"$work/server.err" &
spid=$!
port=
for _ in $(seq 1 100); do
port=$(sed -n 's/^PORT \([0-9]*\)$/\1/p' "$work/server.out" 2>/dev/null || true)
test -n "$port" && break
sleep 0.1
done
test -n "$port" || {
echo "local-server.py did not announce a port" >&2
exit 1
}
# Save path is "127.0.0.1_<port>/" + the URL path; target 998 bytes total.
prefix="127.0.0.1_${port}/"
need=$((998 - ${#prefix}))
# macOS caps PATH_MAX at 1024, so neither the docroot nor the mirror can hold a
# 998-byte path, and httrack's own HTS_MAX_PATH_LEN clamps below it too. The
# trigger is simply not reachable there.
pathmax=$(getconf PATH_MAX / 2>/dev/null || echo 4096)
test "$pathmax" -ge $((need + ${#work} + 128)) || {
echo "PATH_MAX $pathmax too small for a $need-byte path; skipping" >&2
exit 77
}
# Same on Win32, where MAX_PATH clamps the save path; getconf answers for the
# MSYS emulation, not the Win32 API httrack calls, so it cannot see this.
case "$(uname -s)" in
MINGW* | MSYS* | CYGWIN*)
echo "Win32 MAX_PATH cannot hold a $need-byte save path; skipping" >&2
exit 77
;;
esac
urlpath=""
while test $((need - ${#urlpath})) -gt 60; do
urlpath="${urlpath}$(printf 'd%02d' ${#urlpath})$(printf 'a%.0s' $(seq 1 46))/"
done
urlpath="${urlpath}$(printf 'f%.0s' $(seq 1 $((need - ${#urlpath} - 5)))).html"
test ${#urlpath} -eq "$need"
mkdir -p "$work/root/$(dirname "$urlpath")"
printf '<html><body>leaf</body></html>\n' >"$work/root/$urlpath"
printf '<html><body><a href="/%s">x</a></body></html>\n' "$urlpath" \
>"$work/root/index.html"
cd "$work/proj" # no -O: opt->path_html stays empty
"$httrack_bin" "http://127.0.0.1:$port/" -q -%v0 -s0 -r3 >"$work/crawl1.log" 2>&1 || true
lst=$work/proj/hts-cache/new.lst
test -f "$lst" || {
echo "pass 1 produced no new.lst" >&2
exit 1
}
# Arms the test: without a line of exactly 1000 bytes linput() never splits and
# the purge never sees a one-byte tail.
awk 'length == 1000 { found = 1 } END { exit !found }' "$lst" || {
echo "no 1000-byte line in new.lst; the chunk split is not exercised" >&2
awk '{ print length }' "$lst" | sort -n | tail -3 >&2
echo "--- urlpath len ${#urlpath}, prefix $prefix" >&2
tail -20 "$work/crawl1.log" >&2
exit 1
}
# Pass 2 renames new.lst to old.lst and runs it through the purge loop.
"$httrack_bin" "http://127.0.0.1:$port/" -q -%v0 -s0 -r3 >"$work/crawl2.log" 2>&1
test -s "$work/proj/$prefix$urlpath" || {
echo "the mirrored leaf did not survive the purge" >&2
exit 1
}

View File

@@ -0,0 +1,27 @@
#!/bin/bash
#
# A .bak left behind by a killed run must not disable the #77 re-fetch backup
# (#758): a leftover file (slow.bin.bak) has to be clobbered so the -M abort can
# still restore the pass-1 copy, and a leftover the engine cannot clobber
# (fast.bin.bak, planted as a directory) has to be reported instead of silently
# truncating with no safety net. Only the Windows leg arms the first half:
# POSIX rename() clobbers on its own.
set -euo pipefail
: "${top_srcdir:=..}"
bash "$top_srcdir/tests/local-crawl.sh" \
--plant-file bigtrunc/slow.bin.bak \
--plant-dir bigtrunc/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' \
--log-found 'could not back up .*fast\.bin' \
--log-not-found 'could not back up .*slow\.bin' \
--log-not-found 'could not restore' \
--found bigtrunc/slow.bin \
--found bigtrunc/fast.bin \
--file-min-bytes bigtrunc/slow.bin 655360 \
--file-not-matches bigtrunc/slow.bin 'stale-leftover' \
httrack 'BASEURL/bigtrunc/index.html' -c2

View File

@@ -0,0 +1,164 @@
#!/bin/bash
#
# An FTP re-fetch used to truncate the mirror before the transfer, so a failed
# one destroyed the previous copy (#771). keep.bin is cut short and empty.bin
# gets nothing; both must keep their old bytes. stay.bin takes the same path and
# completes, so a fix that merely stopped writing would fail here.
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"
report="${out}/hts-changes.json"
mkdir -p "$root" "$out"
# The two generations differ in length, or a re-fetch that resumed at EOF
# instead of truncating would leave the same bytes and pass for the wrong reason.
write_bodies() {
local gen="$1" fill="$2" name
for name in keep empty stay; do
"$python" -c 'import sys; sys.stdout.buffer.write(
("%s-FTP-%s " % (sys.argv[1].upper(), sys.argv[2])).encode()
+ sys.argv[2][-1].encode() * int(sys.argv[3]))' "$name" "$gen" "$fill" \
>"${root}/${name}.bin"
done
}
write_bodies V1 4096
: >"$mode"
serverlog="${tmpdir}/server.out"
"$python" "$server" --root "$(nativepath "$root")" \
--mode-file "$(nativepath "$mode")" >"$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}"
urls=()
for name in keep empty stay; do
urls+=("ftp://127.0.0.1:${port}/${name}.bin")
done
# -c1: a parallel FTP crawl loses whole transfers to a separate, older bug in
# the backlog slot swap (#797), which would flake this test on a loaded runner.
common=(--quiet --disable-security-limits --robots=0 --timeout=20 --max-time=120
--retries=1 -c1 --changes)
fail() {
echo "FAIL: $*" >&2
test ! -f "$report" || cat "$report" >&2
exit 1
}
ok() { echo "OK: $*"; }
size_of() { wc -c <"$1" | tr -d '[:space:]'; }
# Files listed under a report bucket, one "file:size" per line.
listed() {
"$python" - "$report" "$1" <<'EOF' | tr -d '\r'
import json
import sys
with open(sys.argv[1], encoding="utf-8") as fp:
report = json.load(fp)
for entry in report[sys.argv[2]]:
print("%s:%s" % (entry["file"], entry.get("size", "none")))
EOF
}
# --- pass 1: a healthy mirror ------------------------------------------------
# Bounded like every local-crawl.sh pass: a wedged crawl must fail the test, not
# hang the job until CI cancels it and discards the log (#796).
run_with_timeout 300 httrack "${urls[@]}" -O "$out" "${common[@]}" \
>"${tmpdir}/log1" 2>&1
for name in keep empty stay; do
test -s "${out}/${host}/${name}.bin" || fail "pass 1 did not mirror ${name}.bin"
done
mkdir "${tmpdir}/snap"
cp "${out}/${host}"/*.bin "${tmpdir}/snap/"
ok "pass 1 mirrored three files"
# --- pass 2: the same three re-fetched, two of them failing -------------------
# norest throughout: an accepted REST sends the client down the append path,
# which is not the one that truncates.
write_bodies V2 6000
cat >"$mode" <<'EOF'
/keep.bin truncate norest
/empty.bin empty norest
/stay.bin norest
EOF
run_with_timeout 300 httrack "${urls[@]}" -O "$out" "${common[@]}" --update \
>"${tmpdir}/log2" 2>&1
# Both failures have to be the transfer dying mid-body: a connect or lookup
# failure never opens the file, and everything below would pass vacuously.
for name in keep empty; do
grep -aqE "Error:.*\"FTP file incomplete\".*${name}\.bin" "${out}/hts-log.txt" ||
fail "${name}.bin did not fail during the transfer"
done
ok "both re-fetches reached the body and failed there"
for name in keep empty; do
cmp -s "${out}/${host}/${name}.bin" "${tmpdir}/snap/${name}.bin" ||
fail "${name}.bin differs from the copy pass 1 left ($(size_of \
"${out}/${host}/${name}.bin") bytes now)"
done
ok "a failed FTP transfer left the previously mirrored bytes alone"
grep -aq "FTP-V2 " "${out}/${host}/stay.bin" ||
fail "stay.bin was not refreshed; the re-fetch writes nothing at all now"
ok "a successful FTP re-fetch still replaces the file"
leftover=$(find "${out}/${host}" -name '*.bak')
test -z "$leftover" || fail "backup left behind: ${leftover}"
ok "no backup temporary survived the pass"
# --purge-old is on by default: a kept file the run never replaced must still be
# in new.lst, or it is deleted as gone (#746).
for name in keep empty stay; do
test -f "${out}/${host}/${name}.bin" || fail "${name}.bin was purged"
done
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)"
for name in keep empty; do
listed unchanged | grep -qx "${host}/${name}.bin:$(size_of "${tmpdir}/snap/${name}.bin")" ||
fail "${name}.bin is not reported unchanged at its previous size"
if listed changed | grep -q "^${host}/${name}.bin:"; then
fail "${name}.bin is reported changed as well"
fi
done
listed changed | grep -qx "${host}/stay.bin:$(size_of "${out}/${host}/stay.bin")" ||
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

@@ -0,0 +1,13 @@
#!/bin/bash
#
# A URL past the header-format buffer must still produce a record: the archive
# used to lose it whole, with no error and an exit status of 0 (#785).
set -e
: "${top_srcdir:=..}"
tmp=$(mktemp -d "${TMPDIR:-/tmp}/httrack_longurl.XXXXXX")
trap 'set +e; rm -rf "$tmp"' EXIT
httrack -O "$tmp/out" "-#test=warc-longurl" "$tmp"

View File

@@ -0,0 +1,141 @@
#!/bin/bash
#
# The harness timeout (tests/test-timeout.sh) must name the test it killed, say
# what was still running, and exit 124 -- otherwise a wedge only ever shows up as
# a cancelled CI step, which keeps no log and no artifact.
set -eu
testdir=$(cd "$(dirname "$0")" && pwd)
# shellcheck source=tests/testlib.sh
. "${testdir}/testlib.sh"
driver="${testdir}/test-timeout.sh"
test -r "$driver" || {
echo "no test-timeout.sh next to $0" >&2
exit 1
}
fail() {
echo "FAIL: $*" >&2
test -z "${out:-}" || sed 's/^/ | /' <"$out" >&2
exit 1
}
tmp=$(mktemp -d "${TMPDIR:-/tmp}/httrack_sutmo.XXXXXX") || exit 1
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 ----------------------
cat >"$tmp/90_wedged.test" <<'EOF'
echo "marker: the wedged test started"
sleep 300
EOF
start=$SECONDS
rc=0
HTTRACK_TEST_TIMEOUT=5 bash "$driver" "$tmp/90_wedged.test" >"$out" 2>&1 || rc=$?
elapsed=$((SECONDS - start))
test "$rc" -eq 124 || fail "wedged test reported $rc, want 124"
# Never before the budget, or a slow-but-healthy test would be killed too.
test "$elapsed" -ge 5 || fail "the guard fired early (${elapsed}s of a 5s budget)"
test "$elapsed" -lt 30 || fail "the guard fired late (${elapsed}s)"
grep -q 'marker: the wedged test started' "$out" || fail "the test's own output was lost"
# The header, not a bare name: the process list quotes the test's path too, so a
# wrapper that named nothing would still match that.
grep -q '^===== TIMEOUT: 90_wedged.test exceeded' "$out" ||
fail "the diagnostics do not name the test"
grep -q "own process tree" "$out" || fail "no process list in the diagnostics"
# The budget is read, not hard-coded: well under it, the same shape survives.
printf 'sleep 3\necho "slow but healthy"\n' >"$tmp/92_slow.test"
rc=0
HTTRACK_TEST_TIMEOUT=45 bash "$driver" "$tmp/92_slow.test" >"$out" 2>&1 || rc=$?
test "$rc" -eq 0 || fail "a healthy 3s test under a 45s budget reported $rc"
grep -q 'slow but healthy' "$out" || fail "healthy test output lost"
! grep -q 'TIMEOUT' "$out" || fail "the guard fired on a healthy test"
# The killed tree must really be gone, or the next test inherits its ports.
sleep 1
! grep -q "$tmp/90_wedged.test" <<<"$(ps -A -o args 2>/dev/null)" ||
fail "the wedged test survived the guard"
# --- exit status and output of a healthy test pass straight through ----------
# 77 is automake's SKIP and 99 its hard error, so both have to survive the wrapper.
for want in 0 1 77 99; do
printf 'echo "ran with %s"\nexit %s\n' "$want" "$want" >"$tmp/91_plain.test"
rc=0
HTTRACK_TEST_TIMEOUT=60 bash "$driver" "$tmp/91_plain.test" >"$out" 2>&1 || rc=$?
test "$rc" -eq "$want" || fail "exit $want came back as $rc"
grep -q "ran with $want" "$out" || fail "output lost for exit $want"
! grep -q 'TIMEOUT' "$out" || fail "the guard fired on a test that exited at once"
done
# A budget of 0 disables the guard: the test still runs, and is not killed. Its
# own file, so the assertion cannot ride on whatever the loop above left behind.
printf 'echo "guard off"\nexit 3\n' >"$tmp/93_off.test"
rc=0
HTTRACK_TEST_TIMEOUT=0 bash "$driver" "$tmp/93_off.test" >"$out" 2>&1 || rc=$?
test "$rc" -eq 3 || fail "disabled guard changed the exit status ($rc)"
grep -q 'guard off' "$out" || fail "output lost with the guard disabled"
# --- a wedged crawl yields a symbolized engine stack ------------------------
# The whole point of the dump: name the frame the engine is stuck in. Windows has
# neither half (MSYS signals do not reach a native httrack.exe, and that build has
# no backtrace()), so only assert it where the machinery exists.
if is_windows; then
echo "suite timeout OK (no stack capture on Windows)"
exit 0
fi
command -v httrack >/dev/null || {
echo "SKIP: no httrack on PATH for the stack half" >&2
exit 77
}
# /watchdog/stall never answers, so the crawl wedges for real. CRAWL_DEADLINE is
# raised past our budget so the guard under test is what fires, not local-crawl's.
cat >"$tmp/92_stalled.test" <<EOF
CRAWL_DEADLINE=600 bash "${testdir}/local-crawl.sh" --errors 0 \\
httrack 'BASEURL/watchdog/stall'
EOF
rc=0
HTTRACK_TEST_TIMEOUT=20 bash "$driver" "$tmp/92_stalled.test" >"$out" 2>&1 || rc=$?
if grep -qE 'python3 not found|could not discover server port' "$out"; then
echo "SKIP: local crawl prerequisites missing" >&2
exit 77
fi
test "$rc" -eq 124 || fail "stalled crawl reported $rc, want 124"
if grep -q 'No stack trace available on this OS' "$out"; then
echo "suite timeout OK (no backtrace() on this platform)"
exit 0
fi
# Each platform's mechanism has its own marker, and the marker is what proves the
# whole chain connected up rather than the dump merely running.
case "$(uname -s)" in
Linux)
# sig_fatal writes to the engine's own stderr, so this reaching our log also
# proves the crawl-log salvage worked.
grep -q '^Caught signal ' "$out" || fail "no engine backtrace was salvaged"
;;
Darwin)
grep -q '^Call graph:' "$out" || fail "sample(1) produced no call graph"
;;
*)
grep -q 'no stack mechanism known' "$out" ||
fail "unknown platform reported neither a stack nor its absence"
echo "suite timeout OK (no stack mechanism on $(uname -s))"
exit 0
;;
esac
# A stalled crawl is necessarily inside httpmirror, so these frames must appear.
# Matching "httrack" alone would pass on the process list above.
grep -qE '\b(back_wait|httpmirror|hts_main_internal)\b' "$out" ||
fail "the stack names no engine frame"
echo "suite timeout OK (named the test, dumped a stack, exited 124)"

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

@@ -1,12 +1,14 @@
#!/bin/bash
#
# An update run against a dead server must not destroy the cache: the no-data
# rollback restores the previous hts-cache generation (zip caches lost it).
# rollback restores the previous hts-cache generation (zip caches lost it). The
# live update pass leaves an old.zip behind, so the dead pass rotates onto an
# existing generation instead of into empty space.
set -eu
: "${top_srcdir:=..}"
bash "$top_srcdir/tests/local-crawl.sh" --errors 0 --rerun-dead \
bash "$top_srcdir/tests/local-crawl.sh" --errors 0 --rerun --rerun-dead \
--found 'simple/basic.html' \
httrack 'BASEURL/simple/basic.html'

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

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

View File

@@ -14,7 +14,8 @@ testdir=$(cd "$(dirname "$0")" && pwd)
. "${testdir}/testlib.sh"
tmp=$(mktemp -d "${TMPDIR:-/tmp}/httrack_portsib.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 port the old code accepted only by wrapping: 4295009395 = 2^32 + 42099 and
# 4294967376 = 2^32 + 80 both fold to a plausible listen port. They are the

View File

@@ -14,7 +14,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"
hexseg=$(printf 'a1b2c3d4e5f60718%.0s' {1..8}) # 128 hex chars

View File

@@ -36,7 +36,8 @@ cleanup() {
wait "${srv}" 2>/dev/null || true # absorb bash's async "Killed" notice
rm -rf "${work}" "${srvlog}"
}
trap cleanup EXIT HUP INT QUIT PIPE TERM
trap 'set +e; cleanup' EXIT
trap cleanup HUP INT QUIT PIPE TERM
# webhttrack server on a pre-picked port; an isolated HOME keeps a stray
# ~/.httrack.ini out of it.

View File

@@ -1,11 +1,14 @@
#!/bin/bash
#
# A --warc crawl writes a standards-conformant WARC/1.1 archive, and an
# --update re-crawl of an unchanged (all-304) site emits revisit records.
# The stdlib validator (no warcio) is the real gate: it byte-compares the
# fresh page.html response body against what the server served, asserts the
# encoding headers were stripped and the payload digest matches, then checks
# the update pass turned the unchanged assets into revisits (no full response).
# A --warc crawl writes a standards-conformant WARC/1.1 archive, and a re-crawl
# of an unchanged (all-304) site emits revisit records. The stdlib validator (no
# warcio) is the real gate: it byte-compares the fresh page.html response body
# against what the server served, asserts the encoding headers were stripped and
# the payload digest matches, then checks the second pass turned the unchanged
# assets into revisits (no full response).
#
# That second pass archives under a name of its own, which is also the recovery
# the engine points at when it refuses to strand a previous archive (#759).
set -eu
@@ -14,8 +17,10 @@ set -eu
# page.html body served by tests/local-server.py (route_mini304_page).
export WARC_VALIDATE_BODY="page.html=3c68746d6c3e3c626f64793e74696e7920636163686561626c6520706167653c2f626f64793e3c2f68746d6c3e0a"
export WARC_VALIDATE_NORESP="index.html page.html"
export WARC_VALIDATE_UPDATE="warc-update.warc.gz"
bash "$top_srcdir/tests/local-crawl.sh" --errors 0 --rerun --warc-validate \
bash "$top_srcdir/tests/local-crawl.sh" --errors 0 --warc-validate \
--rerun-args '--warc-file warc-update' \
--log-found 'no files updated' \
--found 'mini304/index.html' --found 'mini304/page.html' \
httrack 'BASEURL/mini304/index.html' --warc-file warc-out

View File

@@ -6,9 +6,10 @@
# the datapackage-digest chain (plus py-wacz/pywb when importable). The crawl
# skips cleanly on a build without OpenSSL (no conformant SHA-256 -> no package).
#
# The cacheless second pass repackages over the .wacz the first one left: the
# clobber, not the happy rename, is what breaks when a platform refuses to
# rename onto an existing file (#726).
# The cacheless (-C0) second pass repackages over the .wacz the first one left:
# the clobber, not the happy rename, is what breaks when a platform refuses to
# rename onto an existing file (#726). A cache-served pass keeps the previous
# package instead, which is 97_local-warc-update-keep.
set -eu

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
cd "$scratch"
# ~300 chars: over the old 236 ceiling, well under any POSIX PATH_MAX. The

View File

@@ -23,7 +23,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
serverlog="${tmpdir}/server.log"
: >"$serverlog"

View File

@@ -32,7 +32,8 @@ cleanup() {
stop
rm -f "${log}"
}
trap cleanup EXIT HUP INT QUIT PIPE TERM
trap 'set +e; cleanup' EXIT
trap cleanup HUP INT QUIT PIPE TERM
freeport() {
python3 -c 'import socket

View File

@@ -26,7 +26,8 @@ cleanup() {
test -z "${srv}" || kill -9 "${srv}" 2>/dev/null || true
rm -f "${log}"
}
trap cleanup EXIT HUP INT QUIT PIPE TERM
trap 'set +e; cleanup' EXIT
trap cleanup HUP INT QUIT PIPE TERM
freeport() {
python3 -c 'import socket

View File

@@ -30,7 +30,7 @@ cleanup() {
stop_server "$ptpid"
rm -rf "$dir"
}
trap cleanup EXIT
trap 'set +e; cleanup' EXIT
# Neither header is present, so file->contenttype and ->lastmodified stay "".
printf 'HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\n' >"$dir/hdr"

View File

@@ -17,7 +17,8 @@ command -v httrack >/dev/null || {
}
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/httrack_symbolize.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
out="${tmpdir}/trace"
raw="${tmpdir}/trace-optout"
overflow="this string is far too long for the buffer"

View File

@@ -32,7 +32,8 @@ cleanup() {
srv=
rm -rf "${work}" "${srvlog}"
}
trap cleanup EXIT HUP INT QUIT PIPE TERM
trap 'set +e; cleanup' EXIT
trap cleanup HUP INT QUIT PIPE TERM
# webhttrack server on a pre-picked port; an isolated HOME keeps a stray
# ~/.httrack.ini out of it.

View File

@@ -38,7 +38,8 @@ cleanup() {
wait "${srv}" 2>/dev/null || true # absorb bash's async "Killed" notice
rm -rf "${work}" "${srvlog}"
}
trap cleanup EXIT HUP INT QUIT PIPE TERM
trap 'set +e; cleanup' EXIT
trap cleanup HUP INT QUIT PIPE TERM
# Stand in for a finished mirror: the crawl itself is not under test.
proj="${work}/websites/proj"

View File

@@ -27,7 +27,8 @@ cleanup() {
test -z "${srv}" || kill -9 "${srv}" 2>/dev/null || true
rm -f "${log}"
}
trap cleanup EXIT HUP INT QUIT PIPE TERM
trap 'set +e; cleanup' EXIT
trap cleanup HUP INT QUIT PIPE TERM
freeport() {
python3 -c 'import socket

View File

@@ -34,7 +34,8 @@ cleanup() {
wait "${csrv}" 2>/dev/null || true # absorb bash's async "Killed" notice
rm -rf "${log}" "${work}"
}
trap cleanup EXIT HUP INT QUIT PIPE TERM
trap 'set +e; cleanup' EXIT
trap cleanup HUP INT QUIT PIPE TERM
freeport() {
python3 -c 'import socket

View File

@@ -28,7 +28,8 @@ cleanup() {
rm -f "${log}"
rm -rf "${base}"
}
trap cleanup EXIT HUP INT QUIT PIPE TERM
trap 'set +e; cleanup' EXIT
trap cleanup HUP INT QUIT PIPE TERM
freeport() {
python3 -c 'import socket

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