Compare commits

...

24 Commits

Author SHA1 Message Date
Xavier Roche
ee550c3972 Bound test 19's dead-address connects so it is fast on macOS
19_local-connect-fallback pins dead loopback IPs (127.0.0.2/.3) before the live
one to exercise address fallback. On Linux those refuse instantly (all of
127.0.0.0/8 is loopback); macOS has only 127.0.0.1, so the connects block to
--timeout, and default retries replay the whole crawl, so the exhaustion case
took ~2 minutes. It passed but dominated the macOS suite once the rest runs in
parallel. Drop --timeout to 5s (bounds the last candidate's stall; the earlier
ones already fall back at min(timeout,10)) and set --retries=0 (no ×3 replay).
Linux is unchanged: the refused connects never reach the timeout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-11 16:54:25 +02:00
Xavier Roche
145ea4769f Make test 31 fail loudly when the java plugin doesn't load (#529)
31_local-javaclass verifies the java plugin parses a .class constant pool
by asserting a resource named only inside Foo.class gets crawled. When the
plugin fails to load, that assertion just reports hello.gif "not found",
with no hint the plugin was the cause -- which is why an intermittent
local failure sat uninvestigated.

The failure mode is a shadowing system libhtsjava: on a box with httrack
installed, if the soname httrack dlopens is absent from the fresh build
but present in /usr/lib, the loader picks up the system plugin (a
mismatched, non-instrumented build) and it silently parses nothing. It
looked ASan-specific only because that was the build in use; clean CI
runners have no system plugin, so they never hit it. #475 aligned the
dlopen name with the build's own soname, closing the gap.

Assert the plugin's launched-banner suffix ("+libhtsjava") first, so a
load failure trips there with a clear message instead of the opaque
hello.gif miss. The "+libhtsjava" prefix survives future soname bumps.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 16:33:45 +02:00
Xavier Roche
c5da5a2f2e Stop test 36 flaking on macOS when loopback drops connections (#527)
* Stop test 36 flaking on macOS when loopback drops connections

The 36_local-bigcrawl crawl runs -c8 over 361 files and asserted its
error count exactly (--errors 4, the planted 404/410/500/gztrunc). On the
macOS CI runner the busy loopback intermittently resets a few in-flight
connections, so the count came back 7 and the job failed while Linux
passed.

Add a --errors-content harness assertion that counts every "Error:" line
minus the transient-network family (statuscodes -2..-7:
timeout/connect/reset), and switch test 36 to it. The four planted
content errors (404/410/500 and the -1 decompression failure) still count
exactly, so a real regression that adds a content/HTTP error still fails;
only loopback flakiness is tolerated.

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

* Tighten --errors-content: drop -7 from transient, match retried errors

Review follow-up. Two fixes to the transient-error filter:

- Narrow the excluded band from -2..-7 to -2..-6. STATUSCODE_TOO_BIG (-7)
  is a deterministic size-cap rejection, not a network transient, so it
  must count as a real error; excluding it could mask a -M regression in
  a future test that reuses this option.

- Also match the "after N retries" error format. htsparse.c logs
  "<msg>" (<code>) after N retries at link ... when retries are enabled,
  which the old "(-code) at link" regex missed. Test 36 forces
  --retries=0 so only the immediate format appears there, but the option
  is a general harness primitive and should hold under default retries.

Co-Authored-By: Claude Opus 4.8 (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 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 15:25:59 +02:00
Xavier Roche
6eaefe6233 Type LLint/TStamp as int64_t and drop the per-platform boilerplate (#526)
* Type LLint/TStamp as int64_t, drop the per-platform boilerplate

The wide-integer typedefs picked long/long long/__int64 per platform and
carried a matching printf-format ladder plus the HTS_LONGLONG capability
macro. That is what let x32 pick a 32-bit long for a "64-bit" LLint (#524).

<stdint.h>/<inttypes.h> answer both questions directly: int64_t is exactly
64-bit signed with defined wrap, and "%" PRId64 is its conversion. LLint
stays signed because -1 is a size/range sentinel across the engine (a
uint64_t would silently break every "< 0" check). The names LLint, TStamp,
and the LLintP macro are kept, so the ~70 call sites are untouched.

HTS_LONGLONG is now dead (a 64-bit type is a hard C99 dependency here:
md5.h includes <stdint.h> and the installed httrack-library.h includes
<inttypes.h>), so its detection blocks and htslib.c's #ifdef around the
GiB/TiB/PiB formatting go too; those branches are always valid now.

No ABI change: int64_t is `long` on LP64, so httrackp/htsblk stay
byte-identical and the exported symbol set is unchanged.

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

* Tighten the LLint typedef comments

Two one-line blocks instead of a four-line header; drop the change-narration
("no per-platform ladder"). Keep the load-bearing note that LLintP carries its
own '%'.

Co-Authored-By: Claude Opus 4.8 (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 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 15:09:54 +02:00
Xavier Roche
33249be5a0 Assert kept content in the update-error test, not just size (#176 follow-up) (#525)
44_local-update-errormask checked only keep.dat's size (>= 1024). The good
body carries a distinctive KEEP marker that nothing asserted, so a same-size
wrong-content overwrite would still pass. Also assert the content matches
^KEEP and lacks the 403 error body, so the test discriminates the fix from
an equal-size clobber the size check alone misses.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 14:49:01 +02:00
Xavier Roche
167ede32c0 Keep the good local copy when an update fetch returns an HTTP error (#176) (#523)
On --update, a URL that returned 200 on the first crawl but now answers a
transient error (403/404/5xx) had its good local copy destroyed: the error
body overwrote it (errpage is on by default) and/or the delete_old purge
removed it. The guard that masks such an error as a 304 to keep the cached
copy was gated on !opt->delete_old, but delete_old is on by default, so it
never ran for a normal update.

Drop that outer gate so the masking runs by default, but restrict it to the
complete-cached-copy case (range_req_size == 0): an error on a resume/range
fetch must still fall through, else a stale-partial 416 would be masked to
304 and the partial never re-fetched (regressing #206). The masked error
routes through the not-modified handler, which reloads and re-registers the
cached file, blocking both the overwrite and the purge. Unlinked-page purging
is unaffected: it targets pages not fetched this run, not a fetched page that
errored.

Test 44_local-update-errormask drives it over a local server whose keep.dat
200s on the first crawl and 403s on the conditional update fetch.

Closes #176

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 14:30:43 +02:00
Xavier Roche
c8b7c63c9b Keep LLint 64-bit on the x32 ABI (#524)
The wide-integer typedef keyed on __x86_64__, which the x32 ABI also
defines even though its long is 32-bit. That silently made LLint -- the
signed 64-bit type for byte counts and file sizes -- 32-bit on x32, so
any value past INT_MAX overflowed (a >2GB size wrapped negative and the
cache dropped the entry). Exclude __ILP32__ so x32 takes the long-long
path, plus a compile-time guard that trips wherever 64-bit support is
claimed but LLint isn't actually 64-bit.

Surfaced by the cache selftests on Debian x32: cache-writefail (oversize)
and cache-corrupt both exercise >2GB entries and failed.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 14:21:27 +02:00
Xavier Roche
04f37b2094 Don't destroy already-complete files when -M/-E aborts an --update (#522)
An --update run stopped by the -M size cap (or -E time cap) could wipe out
files that were already fully mirrored, via two paths:

  - An incomplete re-fetch opens the real file "wb" (truncating the good copy)
    before any body byte arrives; the hard-abort then leaves it truncated.
  - A file that fully arrives is committed, but the aborted slot then reaches
    the main-loop save with statuscode <= 0 and size 0, and filesave() writes a
    zero-byte file over it -- leaving the file empty on disk while the cache
    still records it 200 OK, so a later --update never re-fetches it (permanent
    loss).

For the first path, a re-fetch over an existing file now moves the good copy
aside before truncating; back_finalize commits the fresh file on a full body and
restores the previous copy on an aborted/incomplete transfer. url_sav is still
written the normal way, so the file list, update purge and cache bookkeeping are
unchanged. For the second, an empty body only overwrites the file on a real
response (statuscode > 0), never on a failed transfer.

Adds 43_local-update-truncate.test with a stateful bigtrunc fixture that
mirrors, then re-fetches under --update -M and asserts both the completed and
the incomplete-refetch file keep their full size across the abort. The harness
gains --rerun-args (a second pass with extra args) and --file-min-bytes.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 11:20:29 +02:00
Xavier Roche
c07b19bc3d -M overall-size cap never bounds the overshoot under a slow server (#519)
* -M byte cap only smooth-stopped, never bounded the overshoot (#77)

The overall-size limit (-M / maxsite) requested a smooth stop once
stat_bytes crossed the cap but back_checkmirror always returned 1, so a
slow or throttling server let the in-flight transfers drain at its own
pace and the wait loops starved on them. On a bot-throttled archive the
mirror ran for over an hour and blew far past the cap. This is the size
counterpart of the -E fix in #482, which left the maxsite branch alone.

Give -M the same grace-then-abort escape hatch: once saved bytes overrun
the cap by maxsite/10, back_checkmirror returns 0 and the existing #482
abort path tears down the in-flight HTTP transfers (FTP slots stay with
their thread; real-named partials survive for --continue). The generic
abort log is reworded from "time limit" to "mirror limit" since it now
covers both quotas.

42_local-maxsize-slow.test crawls a server whose files trickle for a
minute under -M400000: the fixed engine stops in about 8 seconds, the
unfixed binary waits the full 60s.

The -M meter itself (saved bytes, which undercounts redirect/plugin-heavy
crawls vs. bytes actually received) is unchanged here; that is a separate
semantic question tracked as a follow-up.

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

* Name the abort cause (size vs time) instead of a generic "mirror limit"

The #482 abort log and per-slot status were reworded to a generic "mirror
limit" once the size cap could also drive them, which dropped the cause. Add
an hts_mirror_limit enum and a back_mirror_limit() helper that reports which
cap overran its grace; back_checkmirror() now delegates its hard-stop decision
to it (removing the duplicated grace conditions), and the back_wait abort loop
uses it to log "size limit reached" / "time limit reached" and stamp the slot
"Mirror Size Limit" / "Mirror Time Out". This restores the precise wording -E
had before and gives the size path its own.

back_maxsize_grace keeps no floor, unlike the time grace: a size overrun
should abort promptly rather than let more bytes through. Documented inline.

Dropped the `--found bigtrickle/p0.bin` check from the test: the #482 hard
abort truncates a large file that is being re-fetched when the cap trips
(reproducible on master's -E path too), so p0.bin is legitimately 0 bytes on
many runs. The wall-clock bound is the discriminating assertion; the "giving
up" line fires for the unfixed engine as well.

Co-Authored-By: Claude Opus 4.8 (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 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 09:17:13 +02:00
Xavier Roche
04bbb489cf Drop the per-request pre-resolve that slowed crawls on non-default ports (#181) (#518)
back_solve() resolved the raw "host:port" authority once per backing
request through check_hostname_dns() — an uncached getaddrinfo whose
result was only logged (host_wait() is always synchronous now). The
connect path already strips the port and resolves through the DNS cache,
so this lookup was pure overhead. On resolvers that don't fail fast on
the malformed "host:port" name (search-domain expansion, DNS timeouts)
it added a per-request stall: the reported slowness. Mirrors on port 80,
or with the port omitted, avoided the bad name and stayed fast.

Remove back_solve() and its debug-only lookup; keep the synchronous
host_wait() gate. Add a DNS self-test pinning that the cache/connect
path resolves "host:port" as the bare host.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 00:13:40 +02:00
Xavier Roche
a24b1d3c9f Release 3.49.12 (#517)
Roll up 25 commits since 3.49.11 (July 5): security fixes in the
network-facing parsers (remote stack overflow and uninitialized read in
Content-Type/-Encoding handling, fuzzer-found over-reads in the filter,
URL and IDNA parsers, cache-index bounds, filter-pattern backtracking,
world-readable cookies.txt), crawl-correctness fixes (double-encoded UTF-8
links, gzip-mislabeled bodies, cache reconcile/corruption handling,
orphaned .delayed placeholders), the new --why filter diagnostic, and
build hardening (_FORTIFY_SOURCE, -fstack-protector-strong).

VERSION_INFO 3:3:0 -> 3:4:0: httrackp tail-appends why_url, no layout or
exported-signature break, so soname stays libhttrack.so.3 (revision bump).

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 21:28:23 +02:00
Xavier Roche
1abf867333 Links with raw UTF-8 bytes are fetched double-encoded (404) (#516)
* Fix double-encoded requests for raw UTF-8 links (#180)

Links holding raw non-ASCII bytes were always converted from the page
charset to UTF-8, even when already UTF-8: with the iso-8859-1 fallback
(reached whenever the charset was declared via HTML5 <meta charset=..>,
which the meta scanner predates), each UTF-8 byte was re-encoded and the
mirror requested %c3%a7%c2%bb... instead of %e7%bb..., saving a 404.

Gate the conversion on a strict (RFC 3629) hts_isStringUTF8, and rewrite
hts_getCharsetFromMeta as a size-bounded attribute scanner that handles
both the HTML5 and http-equiv forms in any attribute order, first tag
wins. Drop the dormant, broken hts_getCharsetFromContentType.

Covered by -#test=metacharset/-#test=isutf8 engine self-tests, an
11-variant local-server crawl matrix with an update pass
(41_local-utf8-link.test), and new fuzz corpus seeds.

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

* tests: international crawl tracked the double-encoded behavior

The default.html sub-crawl asserted the pre-fix output: two mojibake
café*.html filenames from double-encoded fetches and 4 errors where
the fix leaves 2 (the caf%e9.html Latin-1 escapes the server really
lacks). Update to the corrected names and counts; the other three
sub-crawls are unchanged. Only runs where ut.httrack.com is reachable
(in-tree builds), which is why just the deb CI job caught it.

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 21:06:28 +02:00
Xavier Roche
771d11326e Uncompressed body labeled gzip loses the page (#515)
* htszlib: keep an identity body mislabeled as compressed

A server sending Content-Encoding: gzip with a plain uncompressed body
lost the page: both inflate attempts fail, hts_zunpack returns -1, and
the mirror reports "Error when decompressing" with an empty file left
behind. Fall back to copying the body verbatim when neither deflate
framing decodes and the first bytes carry no gzip/zlib header; corrupt
or truncated compressed data still fails. Covered in the acceptencoding
self-test. Follow-up to the #47 investigation.

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

* htszlib: fall back to identity only on a deflate data error

Adversarial review found two over-broad paths to the verbatim copy: a
truncated raw deflate stream (no header, exhausts input without a data
error) and any local failure (fwrite, inflateInit2, fseek, FOPEN) also
reached it, saving compressed garbage as the document with a success
return. Gate the fallback on the raw-deflate attempt hitting a data
error with no environmental failure. Selftest grows a multi-chunk
identity body and truncated gzip/zlib/raw-deflate negatives; the /big/
crawl now serves a mislabeled plain page end-to-end.

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 20:08:12 +02:00
Xavier Roche
7faceaf7d2 Remove the dead HTS_FAST_CACHE==0 code paths (#514)
HTS_FAST_CACHE has been hard-set to 1 since the coucal hashtable became
the cache index; the ==0 arms no longer even compile (they reference an
undeclared variable in cache_readex). Drop the knob, the dead arms, and
the cache_back 'use' field they read, which was never assigned.

No behavior change: the kept code is exactly what the preprocessor
selected, and the removed cache.use cleanup was a never-taken branch.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 19:03:59 +02:00
Xavier Roche
9105732112 strjoker: catastrophic backtracking on *-heavy filter patterns (#513)
* Fix catastrophic backtracking in the strjoker filter matcher

The recursive wildcard matcher re-explored the same (subject, pattern)
positions exponentially on *-heavy patterns against a non-matching
subject: 9 stars against 50 characters already took 149 seconds, about
7x per added star. Subjects come from crawled URLs, so a hostile site
could stall matching against a user's filters (ReDoS).

Memoize failures instead: one bit per (subject, pattern) offset pair,
checked before and recorded after each recursion, which bounds the
work polynomially. A pair that failed once fails forever (*size is
only written on the success path), so the memo is sound and the
matcher's semantics are unchanged; a differential run of 9000 random
pattern/subject/size cases old vs new is byte-identical. The bitmap
lives on the stack for common sizes, on the heap above 2 KB.

The engine test adds the pathological pattern (instant now, hung
before) plus a case where a later star's dead-end must re-extend an
earlier star, which guards against a single-backtrack-point rewrite.
A corpus seed lets the fuzz replay job cover the same shape.

Closes #501

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

* Broaden matching-system test coverage

Adds near-miss negative cases for the arms the filter tests skipped
(word order, exact tails, range boundaries, class case sensitivity,
empty star runs, descending ranges, *[param], rule ordering, failed
size tests), first coverage of mime: filters via a new
-#test=filtermime self-test, and a -#test=filtermemo differential:
20000 seeded random pattern/subject/size cases where the memoized
matcher must agree with a new no-memo oracle (strjoker_nomemo) on
result, *size and *size_flag, asserting both polarities occur.
Corrupting the memo indexing by hand makes the differential fail on
case 8, so it genuinely guards the memo.

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 10:05:07 +02:00
Xavier Roche
9c01812141 cookies.txt is created world-readable (#511)
* Keep cookies.txt owner-only (0600)

cookie_save() wrote the jar with fopen, so live session cookies ended
up world-readable under the usual umask. Create it O_CREAT 0600 on
Unix (new HTS_PROTECT_FILE), fchmod pre-existing jars down to 0600 on
rewrite, and close the fd if fdopen fails. The st_cookies selftest
asserts both the fresh-create and the tighten-on-rewrite mode
(ASan-independent, proven by reverting each fix).

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

* CodeQL: exclude cpp/world-writable-file-creation

The rule flags every file-creating fopen (0666 & ~umask): 53 baseline
alerts over mirror/cache/log/test output where umask-controlled modes
are the intended, conventional behavior. Its one real catch, the
cookies jar, is now kept 0600 explicitly.

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

* st_cookies: assert the saved jar is non-empty

The mode assertions alone would pass a cookie_save that creates an
empty 0600 file and returns 0; check st_size on both saves (proven by
a negative control that skips the write loop).

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 07:00:01 +02:00
Xavier Roche
7a02d5e411 Remove the pre-3.31 (.dat/.ndx) cache import (#512)
The zip cache replaced this format in 3.31 (2003); what remained was an
import-only parser for hostile input, compiled unconditionally, plus a
tail of dead code keyed to it: the CACHE-1.5 writer behind if(1), the
cross-session HTML-MD5 dedup (its store was never written in the zip
era), the legacy legs of the startup rotation (which silently renamed
or deleted a user's .dat/.ndx pair) and of hts_cache_reconcile.

cache_init now detects a legacy pair, logs a clear refusal, leaves the
files untouched, and re-crawls. The cache-legacy self-test pins refusal
for both layouts (--update old.* and ro new.*), no is_update flag, and
untouched files; proven end-to-end against the local server. The
cacheindex self-test and fuzz-cachendx stay: cache_brstr/cache_binput
still back the -#C cache listing.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 21:47:42 +02:00
Xavier Roche
3c7e9fa46d fuzz-header: strip CRs so the harness feeds the receive-loop shape (#510)
The engine's header receive loop reads via cache_binput, which drops
every \r; the harness fed raw lines, so corpus bytes with \r exercised
a shape production code never sees. Strip them per line.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 21:33:48 +02:00
Xavier Roche
a707d4b845 Add libFuzzer harnesses for the HTTP header and cache-index parsers (reland) (#509)
* Add libFuzzer harnesses for the HTTP header and cache-index parsers

P3-5 fuzz Tier-2. Two more harnesses over hostile-input parsers that read
structured bytes into fixed buffers: fuzz-header drives treatfirstline plus
treathead on each response-header line (the Content-Type/-Encoding path hardened
in #506, and the cookie/Location/Content-Range fields); fuzz-cachendx drives the
hts-cache/*.ndx length-prefixed scan that cache_readex_new loads on --update.

fuzz-cachendx found the over-advance fixed in the parent commit; its seed corpus
carries the two crash reproducers as replay regressions. The cache harness stops
at the scan rather than the trailing coucal insert, whose murmur hash trips a
separate pointer-overflow the .ndx parser does not own.

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

* Correct copyright year on the new fuzz harnesses (2026)

fuzz-header.c and fuzz-cachendx.c are new in the 2026 audit cycle;
match the sibling *_selftest.c files rather than the boilerplate 1998.

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 21:44:19 +02:00
Xavier Roche
65c1016a4e Bound the cache-index (.ndx) parser to its buffer (#507)
* Bound the cache-index (.ndx) parser to its buffer

cache_brstr trusted the on-disk length prefix for its cursor advance
(off += i, i up to 32768), so a corrupt or truncated hts-cache/*.ndx
whose declared length overstates the file walked the length-prefixed scan
past the end of the readfile() buffer; the next binput()/strchr() in the
loader then read out of bounds. A cache truncated by a crash or a full
disk, or one handed to --update/--continue, reaches it.

Bound both the copy and the advance in cache_brstr to the bytes actually
present (strnlen up to the terminating NUL), and add cache_binput(), a
binput() that refuses to start a read at or past end-of-buffer, for the
two loader scan loops in htscache.c and htscoremain.c; the latter also
gains the a < end guard the former already had. Regression: -#test=cacheindex
plus 01_engine-cacheindex.test (the deterministic bound check fails on the
old advance; the ASan CI jobs also exercise the scan).

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

* gitignore: ignore dist/ release staging artifacts

httrack-gh-release stages its signed tarballs in dist/ inside the
checkout; only root-level /httrack-*.tar.gz was ignored, so a broad
git add swept the 3.49.10/3.49.11 artifacts into this branch's first
commit (since rewritten out). Ignore the directory.

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

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 21:02:27 +02:00
Xavier Roche
fd2252dc8c Fix remote stack overflow and uninit read in Content-Type/-Encoding parsing (#506)
treathead() parsed the Content-Type and Content-Encoding response-header
values with a width-less sscanf("%s") into a 1100-byte stack scratch,
bounds-checking the result only after the copy. The receive path in
back_wait hands treathead a line buffer of up to 2000 bytes (htsback.c
rcvd[2048], binput cap 2000), so a hostile server sending a header value
longer than 1099 non-whitespace bytes overflowed the stack. Bound both
scans to the buffer (%1099s), matching the existing %31s/%255s idiom in
the request-line parsers.

The Content-Encoding branch also ignored the sscanf return, so an empty
value (e.g. "Content-Encoding:") left tempo uninitialized and then ran
strlen() over it: an uninitialized read that can also over-read past the
buffer. The Content-Type branch already guarded this; mirror it.

Found via the P3-3 static-analysis baseline. New -#test=headerlong plus
empty-value cases in 01_engine-header.test build the hostile inputs in
the handler (the CLI caps argument length); they abort on the pre-fix
binary under ASan (overflow) and MSan (uninitialized read).

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 08:53:57 +02:00
Xavier Roche
53a257448f Add --why to explain which filter rule accepts or rejects a URL (#505)
The top user-confusion class is filters silently not doing what their
author expects. --why <url>, given the rest of the crawl command line,
prints the deciding rule ("rejected by rule #2 (-*.zip)") or reports
that no rule matches, then exits without crawling. It reuses the real
option parsing and filter seeding end to end, and evaluates through the
new fa_strjoker_dual(), which also replaces the two hand-rolled copies
of the dual-form verdict merge in htswizard.c.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 20:09:25 +02:00
Xavier Roche
8ba3cb6c03 Enable _FORTIFY_SOURCE and -fstack-protector-strong (#504)
The rest of the OpenSSF hardening set (stack-clash, cf-protection,
relro/now, noexecstack, PIE, format-security) is already enabled;
this fills the two gaps. Bundle autoconf-archive's
AX_ADD_FORTIFY_SOURCE (serial 10: tries =3, falls back to =2, no-ops
when the toolchain predefines it); distro copies can be too old
(Debian trixie ships serial 4, =2 only). Skip fortify in -fsanitize
builds, whose interceptors want the unfortified calls. Upgrade
-fstack-protector to -strong, keeping the plain form as fallback.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 20:04:09 +02:00
Xavier Roche
b9afe755bd Add a CodeQL C analysis workflow (#503)
Scan on push to master, PRs, and a weekly cron, with the security-extended
query suite; findings land in the code-scanning dashboard. Manual build mode
so CodeQL traces the real autotools build.

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 20:03:28 +02:00
64 changed files with 2124 additions and 1522 deletions

63
.github/workflows/codeql.yml vendored Normal file
View File

@@ -0,0 +1,63 @@
# CodeQL static analysis (C). The security-extended suite covers the classes
# this codebase actually fights: overflows, tainted-size allocs, format bugs.
name: CodeQL
on:
push:
branches: [master]
pull_request:
schedule:
# Weekly re-scan of master so new/updated queries land without a push.
- cron: "17 4 * * 1"
permissions:
contents: read
concurrency:
group: codeql-${{ github.ref }}
cancel-in-progress: true
jobs:
analyze:
name: analyze (c-cpp)
runs-on: ubuntu-24.04
permissions:
contents: read
# Upload findings to the repo's code-scanning dashboard.
security-events: write
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- name: Install build dependencies
run: |
set -euo pipefail
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential autoconf automake libtool autoconf-archive \
zlib1g-dev libssl-dev
- uses: github/codeql-action/init@v4
with:
languages: c-cpp
build-mode: manual
queries: security-extended
# fopen's umask-controlled 0666 is intended for mirror/cache/log
# output; the one credential file (cookies.txt) is kept 0600 on Unix.
config: |
query-filters:
- exclude:
id: cpp/world-writable-file-creation
# Manual build: CodeQL traces the compiler, so build exactly what ships.
- name: Build
run: |
set -euo pipefail
autoreconf -fi
./configure
make -j"$(nproc)"
- uses: github/codeql-action/analyze@v4
with:
category: "/language:c-cpp"

3
.gitignore vendored
View File

@@ -34,8 +34,9 @@ Makefile
*.so.*
*.a
# make dist output.
# make dist output; dist/ holds httrack-gh-release staging artifacts.
/httrack-*.tar.gz
/dist/
# Editor / autotools backup files.
*~

View File

@@ -1,6 +1,6 @@
AC_PREREQ([2.71])
AC_INIT([httrack], [3.49.11], [roche+packaging@httrack.com], [httrack], [http://www.httrack.com/])
AC_INIT([httrack], [3.49.12], [roche+packaging@httrack.com], [httrack], [http://www.httrack.com/])
AC_COPYRIGHT([
HTTrack Website Copier, Offline Browser for Windows and Unix
Copyright (C) 1998-2015 Xavier Roche and other contributors
@@ -29,11 +29,11 @@ AC_CONFIG_SRCDIR(src/httrack.c)
AC_CONFIG_MACRO_DIR([m4])
AC_CONFIG_HEADERS(config.h)
AM_INIT_AUTOMAKE([subdir-objects])
# 3:3:0: 3.49.11 only adds enum values, macros and inline helpers to the
# installed headers (no struct layout or exported signature changed vs
# 3.49.10), so it stays soname .so.3; bump revision.
# 3:4:0: 3.49.12 tail-appends one field (why_url) to httrackp and otherwise only
# deletes dead comments; no struct layout or exported signature broke vs 3.49.11,
# so it stays soname .so.3; bump revision.
# (3:0:0 was the htsblk mime-buffer widening, the ABI break that moved .so.2 -> .so.3.)
VERSION_INFO="3:3:0"
VERSION_INFO="3:4:0"
AM_MAINTAINER_MODE
AC_USE_SYSTEM_EXTENSIONS
@@ -102,7 +102,8 @@ AX_CHECK_COMPILE_FLAG([-Wignored-qualifiers], [DEFAULT_CFLAGS="$DEFAULT_CFLAGS -
AX_CHECK_COMPILE_FLAG([-Werror=attribute-warning], [DEFAULT_CFLAGS="$DEFAULT_CFLAGS -Werror=attribute-warning"])
AX_CHECK_COMPILE_FLAG([-Werror=user-defined-warnings], [DEFAULT_CFLAGS="$DEFAULT_CFLAGS -Werror=user-defined-warnings"])
AX_CHECK_COMPILE_FLAG([-fstrict-aliasing -Wstrict-aliasing], [DEFAULT_CFLAGS="$DEFAULT_CFLAGS -fstrict-aliasing -Wstrict-aliasing"])
AX_CHECK_COMPILE_FLAG([-fstack-protector], [DEFAULT_CFLAGS="$DEFAULT_CFLAGS -fstack-protector"])
AX_CHECK_COMPILE_FLAG([-fstack-protector-strong], [DEFAULT_CFLAGS="$DEFAULT_CFLAGS -fstack-protector-strong"],
[AX_CHECK_COMPILE_FLAG([-fstack-protector], [DEFAULT_CFLAGS="$DEFAULT_CFLAGS -fstack-protector"])])
AX_CHECK_COMPILE_FLAG([-fstack-clash-protection], [DEFAULT_CFLAGS="$DEFAULT_CFLAGS -fstack-clash-protection"])
AX_CHECK_COMPILE_FLAG([-fcf-protection], [DEFAULT_CFLAGS="$DEFAULT_CFLAGS -fcf-protection"])
AX_CHECK_LINK_FLAG([-Wl,--discard-all], [DEFAULT_LDFLAGS="$DEFAULT_LDFLAGS -Wl,--discard-all"])
@@ -110,6 +111,13 @@ AX_CHECK_LINK_FLAG([-Wl,--no-undefined], [DEFAULT_LDFLAGS="$DEFAULT_LDFLAGS -Wl,
AX_CHECK_LINK_FLAG([-Wl,-z,relro,-z,now], [DEFAULT_LDFLAGS="$DEFAULT_LDFLAGS -Wl,-z,relro,-z,now"])
AX_CHECK_LINK_FLAG([-Wl,-z,noexecstack], [DEFAULT_LDFLAGS="$DEFAULT_LDFLAGS -Wl,-z,noexecstack"])
# Fortify libc calls (=3, else =2) unless the toolchain predefines it; skip
# sanitizer builds, whose interceptors want the unfortified calls.
case "$CFLAGS" in
*-fsanitize=*) ;;
*) AX_ADD_FORTIFY_SOURCE ;;
esac
# Force libc back into DT_NEEDED for libraries that reach it only through
# libhttrack (libhtsjava, the libtest callbacks), but only with a GNU-style
# linker; Apple ld rejects these flags and links libSystem unconditionally.

10
debian/changelog vendored
View File

@@ -1,3 +1,13 @@
httrack (3.49.12-1) unstable; urgency=medium
* New upstream release: security and crawl-correctness fixes (remote stack
overflow in Content-Type/-Encoding parsing, fuzzer-found parser over-reads,
world-readable cookies.txt, filter-pattern denial of service) plus a new
--why filter diagnostic; full list in history.txt.
* Build with _FORTIFY_SOURCE and -fstack-protector-strong.
-- Xavier Roche <xavier@debian.org> Fri, 10 Jul 2026 21:11:09 +0200
httrack (3.49.11-1) unstable; urgency=medium
* New upstream release: crawl correctness and security fixes (network-facing

View File

@@ -1,7 +1,7 @@
# libFuzzer harnesses; built only with --enable-fuzzers (requires clang).
if FUZZERS
noinst_PROGRAMS = fuzz-charset fuzz-meta fuzz-idna fuzz-entities \
fuzz-unescape fuzz-filters fuzz-url
fuzz-unescape fuzz-filters fuzz-url fuzz-header fuzz-cachendx
endif
AM_CPPFLAGS = \
@@ -23,6 +23,8 @@ fuzz_entities_SOURCES = fuzz-entities.c fuzz.h
fuzz_unescape_SOURCES = fuzz-unescape.c fuzz.h
fuzz_filters_SOURCES = fuzz-filters.c fuzz.h
fuzz_url_SOURCES = fuzz-url.c fuzz.h
fuzz_header_SOURCES = fuzz-header.c fuzz.h
fuzz_cachendx_SOURCES = fuzz-cachendx.c fuzz.h
# List corpus files explicitly: automake does not expand EXTRA_DIST globs.
EXTRA_DIST = README.md run-fuzzers.sh \
@@ -34,5 +36,10 @@ EXTRA_DIST = README.md run-fuzzers.sh \
corpus/unescape/percent.txt \
corpus/filters/filter.bin corpus/filters/filter-size.bin \
corpus/filters/regress-empty-subject-unique.bin \
corpus/filters/redos-star-classes.bin \
corpus/url/http-url.txt corpus/url/relative-path.txt \
corpus/url/regress-file-empty-path.txt corpus/url/regress-long-path-abort.txt
corpus/url/regress-file-empty-path.txt corpus/url/regress-long-path-abort.txt \
corpus/header/full-response.txt corpus/header/redirect.txt \
corpus/cachendx/new-format.txt corpus/cachendx/old-format.txt \
corpus/cachendx/regress-overadvance.bin \
corpus/cachendx/regress-truncated-entry.bin

View File

@@ -0,0 +1,7 @@
8
CACHE-1.1
28
Mon, 01 Jan 2024 00:00:00 GMT
www.example.com
/index.html
123

View File

@@ -0,0 +1,5 @@
3
1.0
www.example.com
/page
5

View File

@@ -0,0 +1,2 @@
32768
CACHE-1.1

View File

@@ -0,0 +1,5 @@
8
CACHE-1.1
1
x
www.example.com

Binary file not shown.

View File

@@ -0,0 +1,10 @@
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 1234
Content-Encoding: gzip
Last-Modified: Mon, 01 Jan 2024 00:00:00 GMT
Etag: "abc"
Location: http://example.com/x
Set-Cookie: ID=42; path=/; domain=.example.com
Content-Range: bytes 0-99/100
Transfer-Encoding: chunked

View File

@@ -0,0 +1,3 @@
HTTP/1.0 301 Moved
Location: /elsewhere
Content-Disposition: attachment; filename="a.pdf"

View File

@@ -0,0 +1 @@
<meta content="text/html; charset=gb2312" http-equiv="content-type">

View File

@@ -0,0 +1 @@
<meta charset='utf-8

View File

@@ -0,0 +1 @@
<meta charset = utf-8 />

65
fuzz/fuzz-cachendx.c Normal file
View File

@@ -0,0 +1,65 @@
/* ------------------------------------------------------------ */
/*
HTTrack Website Copier, Offline Browser for Windows and Unix
Copyright (C) 2026 Xavier Roche and other contributors
SPDX-License-Identifier: GPL-3.0-or-later
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Ethical use: we kindly ask that you NOT use this software to harvest email
addresses or to collect any other private information about people. Doing so
would dishonor our work and waste the many hours we have spent on it.
Please visit our Website: http://www.httrack.com
*/
/* Fuzz the cache-index (.ndx) parser: a corrupt or truncated index must not
walk the length-prefixed cache_brstr/cache_binput scan past the buffer.
Mirrors the -#C cache-listing scan (htscoremain.c). */
#include "fuzz.h"
#include "htscache.h"
#include "htslib.h"
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
char *buf = fuzz_strdup(data, size);
const char *const end = buf + size;
char firstline[256];
char *a = buf;
/* header: two length-prefixed fields (version, last-modified) */
a += cache_brstr(a, firstline, sizeof(firstline));
a += cache_brstr(a, firstline, sizeof(firstline));
/* body: newline-delimited host/file/position triples; the length-prefixed
scan must stay inside the buffer */
while (a != NULL && a < end) {
char BIGSTK line[HTS_URLMAXSIZE * 2];
char linepos[256];
int pos;
a = strchr(a + 1, '\n');
if (a == NULL)
break;
a++;
a += cache_binput(a, end, line, HTS_URLMAXSIZE);
a += cache_binput(a, end, line + strlen(line), HTS_URLMAXSIZE);
a += cache_binput(a, end, linepos, 200);
sscanf(linepos, "%d", &pos);
(void) pos;
}
freet(buf);
return 0;
}

74
fuzz/fuzz-header.c Normal file
View File

@@ -0,0 +1,74 @@
/* ------------------------------------------------------------ */
/*
HTTrack Website Copier, Offline Browser for Windows and Unix
Copyright (C) 2026 Xavier Roche and other contributors
SPDX-License-Identifier: GPL-3.0-or-later
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Ethical use: we kindly ask that you NOT use this software to harvest email
addresses or to collect any other private information about people. Doing so
would dishonor our work and waste the many hours we have spent on it.
Please visit our Website: http://www.httrack.com
*/
/* Fuzz the HTTP response-header parser (htslib.c): treatfirstline on the
status line, treathead on each following header. Both consume raw bytes
off the wire and copy fields into fixed htsblk buffers; treathead also
mutates its line in place and drives cookie parsing. */
#include "fuzz.h"
#include "htslib.h"
#include "htsbauth.h"
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
char *buf = fuzz_strdup(data, size);
htsblk r;
t_cookie *cookie = calloct(1, sizeof(*cookie));
char *line = malloct(size + 1);
char *p = buf;
int first = 1;
memset(&r, 0, sizeof(r));
cookie->max_len = (int) sizeof(cookie->data);
r.location = malloct(HTS_URLMAXSIZE * 2);
r.location[0] = '\0';
/* feed one header line at a time, as the receive loop does */
while (p != NULL && *p != '\0') {
char *nl = strchr(p, '\n');
size_t n = (nl != NULL) ? (size_t) (nl - p) : strlen(p);
size_t i, len = 0;
/* binput drops every '\r' on the wire; mirror it */
for (i = 0; i < n; i++)
if (p[i] != '\r')
line[len++] = p[i];
line[len] = '\0';
if (first) {
treatfirstline(&r, line);
first = 0;
} else {
treathead(cookie, "www.example.com", "/", &r, line);
}
p = (nl != NULL) ? nl + 1 : NULL;
}
freet(r.location);
freet(line);
freet(cookie);
freet(buf);
return 0;
}

View File

@@ -2,8 +2,8 @@
# Drive every built harness against its seed corpus.
# run-fuzzers.sh <build-fuzz-dir> check deterministic replay (CI smoke)
# run-fuzzers.sh <build-fuzz-dir> [seconds] timed mutation run (discovery)
# Replay is crash/leak-only and never mutates, so it can't hit strjoker's
# catastrophic backtracking; the timed mode can, hence the per-unit -timeout.
# Replay is crash/leak-only and never mutates; the per-unit -timeout guards
# both modes against pathological slowdowns (e.g. pre-#501 strjoker).
set -euo pipefail
srcdir=$(cd "$(dirname "$0")" && pwd)

View File

@@ -4,6 +4,23 @@ HTTrack Website Copier release history:
This file lists all changes and fixes that have been made for HTTrack
3.49-12
+ New: --why explains which filter rule accepts or rejects a given URL, then exits (#505)
+ Fixed: links carrying raw UTF-8 bytes were fetched double-encoded and 404'd (#516)
+ Fixed: an uncompressed body mislabeled as gzip no longer loses the page (#515)
+ Fixed: remote stack overflow and uninitialized read in Content-Type/-Encoding parsing (#506)
+ Fixed: several over-reads and a leak in the filter, URL and IDNA parsers (#499)
+ Fixed: bound the cache-index (.ndx) parser to its buffer (#507)
+ Fixed: catastrophic backtracking on '*'-heavy filter patterns (#513)
+ Fixed: cookies.txt was created world-readable (#511)
+ Fixed: a single corrupt cache entry no longer aborts the whole mirror (#494)
+ Fixed: cache-reconcile policy was broken for zip caches (#491, #493, #495)
+ Fixed: cancelling a crawl mid type-check no longer orphans .delayed placeholders (#496)
+ Fixed: detect URLs after the first inline script and in mid-tag attributes (#497)
+ Changed: build with _FORTIFY_SOURCE and -fstack-protector-strong (#504)
+ Changed: removed the pre-3.31 (.dat/.ndx) cache import (#512)
+ Changed: multiple internal hardening and build improvements (libFuzzer harnesses, CodeQL, dead-code removal)
3.49-11
+ New: parse robots.txt Allow rules and path wildcards per RFC 9309 (#452)
+ New: advertise deflate in Accept-Encoding and decode deflate responses (#450)

119
m4/ax_add_fortify_source.m4 Normal file
View File

@@ -0,0 +1,119 @@
# ===========================================================================
# https://www.gnu.org/software/autoconf-archive/ax_add_fortify_source.html
# ===========================================================================
#
# SYNOPSIS
#
# AX_ADD_FORTIFY_SOURCE
#
# DESCRIPTION
#
# Check whether -D_FORTIFY_SOURCE=2 can be added to CPPFLAGS without macro
# redefinition warnings, other cpp warnings or linker. Some distributions
# (such as Ubuntu or Gentoo Linux) enable _FORTIFY_SOURCE globally in
# their compilers, leading to unnecessary warnings in the form of
#
# <command-line>:0:0: error: "_FORTIFY_SOURCE" redefined [-Werror]
# <built-in>: note: this is the location of the previous definition
#
# which is a problem if -Werror is enabled. This macro checks whether
# _FORTIFY_SOURCE is already defined, and if not, adds -D_FORTIFY_SOURCE=2
# to CPPFLAGS.
#
# Newer mingw-w64 msys2 package comes with a bug in
# headers-git-7.0.0.5546.d200317d-1. It broke -D_FORTIFY_SOURCE support,
# and would need -lssp or -fstack-protector. See
# https://github.com/msys2/MINGW-packages/issues/5803. Try to actually
# link it.
#
# LICENSE
#
# Copyright (c) 2017 David Seifert <soap@gentoo.org>
# Copyright (c) 2019, 2023 Reini Urban <rurban@cpan.org>
#
# Copying and distribution of this file, with or without modification, are
# permitted in any medium without royalty provided the copyright notice
# and this notice are preserved. This file is offered as-is, without any
# warranty.
#serial 10
AC_DEFUN([AX_ADD_FORTIFY_SOURCE],[
ac_save_cflags=$CFLAGS
ac_cwerror_flag=yes
AX_CHECK_COMPILE_FLAG([-Werror],[CFLAGS="$CFLAGS -Werror"])
ax_add_fortify_3_failed=
AC_MSG_CHECKING([whether to add -D_FORTIFY_SOURCE=3 to CPPFLAGS])
AC_LINK_IFELSE([
AC_LANG_PROGRAM([],
[[
#ifndef _FORTIFY_SOURCE
return 0;
#else
_FORTIFY_SOURCE_already_defined;
#endif
]]
)],
AC_LINK_IFELSE([
AC_LANG_SOURCE([[
#define _FORTIFY_SOURCE 3
#include <string.h>
int main(void) {
char *s = " ";
strcpy(s, "x");
return strlen(s)-1;
}
]]
)],
[
AC_MSG_RESULT([yes])
CFLAGS=$ac_save_cflags
CPPFLAGS="$CPPFLAGS -D_FORTIFY_SOURCE=3"
], [
AC_MSG_RESULT([no])
ax_add_fortify_3_failed=1
],
),
[
AC_MSG_RESULT([no])
ax_add_fortify_3_failed=1
])
if test -n "$ax_add_fortify_3_failed"
then
AC_MSG_CHECKING([whether to add -D_FORTIFY_SOURCE=2 to CPPFLAGS])
AC_LINK_IFELSE([
AC_LANG_PROGRAM([],
[[
#ifndef _FORTIFY_SOURCE
return 0;
#else
_FORTIFY_SOURCE_already_defined;
#endif
]]
)],
AC_LINK_IFELSE([
AC_LANG_SOURCE([[
#define _FORTIFY_SOURCE 2
#include <string.h>
int main(void) {
char *s = " ";
strcpy(s, "x");
return strlen(s)-1;
}
]]
)],
[
AC_MSG_RESULT([yes])
CFLAGS=$ac_save_cflags
CPPFLAGS="$CPPFLAGS -D_FORTIFY_SOURCE=2"
], [
AC_MSG_RESULT([no])
CFLAGS=$ac_save_cflags
],
),
[
AC_MSG_RESULT([no])
CFLAGS=$ac_save_cflags
])
fi
])

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 "27 June 2026" "httrack website copier"
.TH httrack 1 "07 July 2026" "httrack website copier"
.SH NAME
httrack \- offline browser : copy websites to a local directory
.SH SYNOPSIS
@@ -51,6 +51,7 @@ httrack \- offline browser : copy websites to a local directory
[ \fB\-%T, \-\-utf8\-conversion\fR ]
[ \fB\-bN, \-\-cookies[=N]\fR ]
[ \fB\-%K, \-\-cookies\-file\fR ]
[ \fB\-%Y, \-\-why\fR ]
[ \fB\-u, \-\-check\-type[=N]\fR ]
[ \fB\-j, \-\-parse\-java[=N]\fR ]
[ \fB\-sN, \-\-robots[=N]\fR ]
@@ -218,6 +219,8 @@ links conversion to UTF\-8 (\-\-utf8\-conversion)
accept cookies in cookies.txt (0=do not accept,* 1=accept) (\-\-cookies[=N])
.IP \-%K
load extra cookies from a Netscape cookies.txt (\-\-cookies\-file <param>)
.IP \-%Y
explain which filter rule accepts or rejects a URL, then exit (\-\-why <param>)
.IP \-u
check document type if unknown (cgi,asp..) (u0 don't check, * u1 check but /, u2 check always) (\-\-check\-type[=N])
.IP \-j

View File

@@ -114,6 +114,8 @@ const char *hts_optalias[][4] = {
"strip [host/pattern=]key1,key2,... from URLs"},
{"cookies-file", "-%K", "param1",
"load extra cookies from a Netscape cookies.txt"},
{"why", "-%Y", "param1",
"explain which filter rule accepts or rejects a URL, then exit"},
{"pause", "-%G", "param1",
"random pause of MIN[:MAX] seconds between files"},
{"generate-errors", "-o", "single", ""},

View File

@@ -68,6 +68,15 @@ static int slot_can_be_cached_on_disk(const lien_back * back);
static int slot_can_be_cleaned(const lien_back * back);
static int slot_can_be_finalized(httrackp * opt, const lien_back * back);
/* Which hard quota, if any, is currently aborting the mirror. */
typedef enum {
HTS_MIRROR_LIMIT_NONE = 0,
HTS_MIRROR_LIMIT_SIZE,
HTS_MIRROR_LIMIT_TIME,
} hts_mirror_limit;
static hts_mirror_limit back_mirror_limit(httrackp *opt);
struct_back *back_new(httrackp *opt, int back_max) {
int i;
struct_back *sback = calloct(1, sizeof(struct_back));
@@ -524,22 +533,22 @@ int back_nsoc_overall(const struct_back * sback) {
/* generate temporary file on lien_back */
/* Note: utf-8 */
static int create_back_tmpfile(httrackp * opt, lien_back *const back) {
static int create_back_tmpfile(httrackp *opt, lien_back *const back,
const char *ext) {
// do not use tempnam() but a regular filename
back->tmpfile_buffer[0] = '\0';
if (back->url_sav != NULL && back->url_sav[0] != '\0') {
snprintf(back->tmpfile_buffer, sizeof(back->tmpfile_buffer), "%s.z",
back->url_sav);
snprintf(back->tmpfile_buffer, sizeof(back->tmpfile_buffer), "%s.%s",
back->url_sav, ext);
back->tmpfile = back->tmpfile_buffer;
if (structcheck(back->tmpfile) != 0) {
hts_log_print(opt, LOG_WARNING, "can not create directory to %s",
hts_log_print(opt, LOG_WARNING, "can not create directory to %s",
back->tmpfile);
return -1;
}
} else {
snprintf(back->tmpfile_buffer, sizeof(back->tmpfile_buffer),
"%s/tmp%d.z", StringBuff(opt->path_html_utf8),
opt->state.tmpnameid++);
snprintf(back->tmpfile_buffer, sizeof(back->tmpfile_buffer), "%s/tmp%d.%s",
StringBuff(opt->path_html_utf8), opt->state.tmpnameid++, ext);
back->tmpfile = back->tmpfile_buffer;
}
/* OK */
@@ -547,6 +556,32 @@ static int create_back_tmpfile(httrackp * opt, lien_back *const back) {
return 0;
}
/* Commit or restore a re-fetch backup (#77 follow-up): a re-fetch over an
existing file moved the good copy to back->tmpfile before truncating url_sav.
commit keeps the new file and drops the backup; else restore it so an aborted
transfer leaves the previous copy intact. Skips the zlib .z temp. */
static void back_finalize_backup(httrackp *opt, lien_back *const back,
const hts_boolean commit) {
if (back->tmpfile == NULL || back->r.compressed)
return;
if (commit) {
(void) UNLINK(back->tmpfile); /* new copy is good; drop the backup */
} else {
if (back->r.out != NULL) {
fclose(back->r.out);
back->r.out = NULL;
}
(void) UNLINK(back->url_sav); /* drop the failed partial */
/* On rename failure keep the backup: it still holds the good copy, so
losing it would be worse than an orphaned temp. */
if (RENAME(back->tmpfile, back->url_sav) != 0)
hts_log_print(opt, LOG_WARNING | LOG_ERRNO,
"could not restore %s; previous copy kept as %s",
back->url_sav, back->tmpfile);
}
back->tmpfile = NULL;
}
// objet (lien) téléchargé ou transféré depuis le cache
//
// fermer les paramètres de transfert,
@@ -582,6 +617,7 @@ int back_finalize(httrackp * opt, cache_back * cache, struct_back * sback,
LLintP " got " LLintP "): %s%s", back[p].r.totalsize,
back[p].r.size, back[p].url_adr, back[p].url_fil);
}
back_finalize_backup(opt, &back[p], HTS_FALSE);
return -1;
}
@@ -600,7 +636,7 @@ int back_finalize(httrackp * opt, cache_back * cache, struct_back * sback,
// en mémoire -> passage sur disque
if (!back[p].r.is_write) {
// do not use tempnam() but a regular filename
if (create_back_tmpfile(opt, &back[p]) == 0) {
if (create_back_tmpfile(opt, &back[p], "z") == 0) {
assertf(back[p].tmpfile != NULL);
/* note: tmpfile is utf-8 */
back[p].r.out = FOPEN(back[p].tmpfile, "wb");
@@ -673,6 +709,9 @@ int back_finalize(httrackp * opt, cache_back * cache, struct_back * sback,
}
}
#endif
/* Body fully received: keep the freshly written url_sav, drop the
backup of the previous copy. */
back_finalize_backup(opt, &back[p], HTS_TRUE);
/* Write mode to disk */
if (back[p].r.is_write && back[p].r.adr != NULL) {
freet(back[p].r.adr);
@@ -901,6 +940,9 @@ 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);
return -1;
}
@@ -1646,138 +1688,104 @@ int back_add(struct_back * sback, httrackp * opt, cache_back * cache, const char
}
}
// tester cache
if ((strcmp(adr, "file://")) /* pas fichier */
&&((!test) || (cache->type == 1)) /* cache prioritaire, laisser passer en test! */
&&((strnotempty(save)) || (strcmp(fil, "/robots.txt") == 0))) { // si en test on ne doit pas utiliser le cache sinon telescopage avec le 302..
#if HTS_FAST_CACHE
if ((strcmp(adr, "file://")) /* pas fichier */
&& ((!test) ||
(cache->type == 1)) /* cache prioritaire, laisser passer en test! */
&&
((strnotempty(save)) || (strcmp(fil, "/robots.txt") ==
0))) { // si en test on ne doit pas utiliser le
// cache sinon telescopage avec le 302..
intptr_t hash_pos;
int hash_pos_return = 0;
#else
char *a = NULL;
#endif
#if HTS_FAST_CACHE
if (cache->hashtable) {
#else
if (cache->use) {
#endif
char BIGSTK buff[HTS_URLMAXSIZE * 4];
#if HTS_FAST_CACHE
strcpybuff(buff, adr);
strcatbuff(buff, fil);
hash_pos_return = coucal_read(cache->hashtable, buff, &hash_pos);
#else
buff[0] = '\0';
strcatbuff(buff, "\n");
strcatbuff(buff, adr);
strcatbuff(buff, "\n");
strcatbuff(buff, fil);
strcatbuff(buff, "\n");
a = strstr(cache->use, buff);
#endif
// Ok, noté en cache->. mais bien présent dans le cache ou sur disque?
#if HTS_FAST_CACHE
// negative values when data is not in cache
if (hash_pos_return < 0) {
#else
if (a) {
#endif
if (!test) { // non mode test
#if HTS_FAST_CACHE==0
int pos = -1;
if (!test) { // not test mode
/* note: no check with IS_DELAYED_EXT() enabled - postcheck by
* client please! */
if (save[0] != '\0' && !IS_DELAYED_EXT(save) &&
fsize_utf8(fconv(catbuff, sizeof(catbuff), save)) <=
0) { // final file missing or empty
int found = 0;
a += strlen(buff);
sscanf(a, "%d", &pos); // lire position
#endif
/* It is possible that the file has been moved due to changes in
* build structure */
{
char BIGSTK previous_save[HTS_URLMAXSIZE * 2];
htsblk r;
#if HTS_FAST_CACHE==0
if (pos < 0) { // pas de mise en cache data, vérifier existence
#endif
/* note: no check with IS_DELAYED_EXT() enabled - postcheck by client please! */
if (save[0] != '\0' && !IS_DELAYED_EXT(save) && fsize_utf8(fconv(catbuff, sizeof(catbuff), save)) <= 0) { // fichier final n'existe pas ou est vide!
int found = 0;
/* It is possible that the file has been moved due to changes in build structure */
{
char BIGSTK previous_save[HTS_URLMAXSIZE * 2];
htsblk r;
previous_save[0] = '\0';
r =
cache_readex(opt, cache, adr, fil, /*head */ NULL,
previous_save[0] = '\0';
r = cache_readex(opt, cache, adr, fil, /*head */ NULL,
/*bound to back[p] (temporary) */
back[p].location_buffer, previous_save, /*ro */
1);
/* Is supposed to be on disk only */
if (r.is_write && previous_save[0] != '\0') {
/* Exists, but with another (old) filename: rename (almost) silently */
if (strcmp(previous_save, save) != 0
&& fexist_utf8(fconv(catbuff, sizeof(catbuff), previous_save))) {
rename(fconv(catbuff, sizeof(catbuff), previous_save),
fconv(catbuff2, sizeof(catbuff2), save));
if (fexist_utf8(fconv(catbuff, sizeof(catbuff), save))) {
found = 1;
hts_log_print(opt, LOG_DEBUG,
"File '%s' has been renamed since last mirror to '%s' ; applying changes",
previous_save, save);
} else {
hts_log_print(opt, LOG_ERROR,
"Could not rename '%s' to '%s' ; will have to retransfer it",
previous_save, save);
}
/* Is supposed to be on disk only */
if (r.is_write && previous_save[0] != '\0') {
/* Exists, but with another (old) filename: rename (almost)
* silently */
if (strcmp(previous_save, save) != 0 &&
fexist_utf8(
fconv(catbuff, sizeof(catbuff), previous_save))) {
rename(fconv(catbuff, sizeof(catbuff), previous_save),
fconv(catbuff2, sizeof(catbuff2), save));
if (fexist_utf8(fconv(catbuff, sizeof(catbuff), save))) {
found = 1;
hts_log_print(opt, LOG_DEBUG,
"File '%s' has been renamed since last "
"mirror to '%s' ; applying changes",
previous_save, save);
} else {
hts_log_print(opt, LOG_ERROR,
"Could not rename '%s' to '%s' ; will have "
"to retransfer it",
previous_save, save);
}
}
back[p].location_buffer[0] = '\0';
}
back[p].location_buffer[0] = '\0';
}
/* Not found ? */
if (!found) {
#if HTS_FAST_CACHE
hash_pos_return = 0;
#else
a = NULL;
#endif
// dévalider car non présent sur disque dans structure originale!!!
// sinon, le fichier est ok à priori, mais on renverra un if-modified-since pour
// en être sûr
if (opt->norecatch) { // tester norecatch
if (!fexist_utf8(fconv(catbuff, sizeof(catbuff), save))) { // fichier existe pas mais déclaré: on l'a effacé
FILE *fp = FOPEN(fconv(catbuff, sizeof(catbuff), save), "wb");
/* Not found ? */
if (!found) {
// invalidate: gone from disk, force a refetch
hash_pos_return = 0;
if (opt->norecatch) {
if (!fexist_utf8(fconv(
catbuff, sizeof(catbuff),
save))) { // declared but missing: user erased it
FILE *fp =
FOPEN(fconv(catbuff, sizeof(catbuff), save), "wb");
if (fp)
fclose(fp);
hts_log_print(opt, LOG_WARNING,
"Previous file '%s' not found (erased by user ?), ignoring: %s%s",
save, back[p].url_adr, back[p].url_fil);
}
} else {
if (fp)
fclose(fp);
hts_log_print(opt, LOG_WARNING,
"Previous file '%s' not found (erased by user ?), recatching: %s%s",
"Previous file '%s' not found (erased by "
"user ?), ignoring: %s%s",
save, back[p].url_adr, back[p].url_fil);
}
} else {
hts_log_print(opt, LOG_WARNING,
"Previous file '%s' not found (erased by user "
"?), recatching: %s%s",
save, back[p].url_adr, back[p].url_fil);
}
} // fsize() <= 0
#if HTS_FAST_CACHE==0
}
#endif
}
} // fsize() <= 0
}
}
//
} else {
#if HTS_FAST_CACHE
hash_pos_return = 0;
#else
a = NULL;
#endif
}
// Existe pas en cache, ou bien pas de cache présent
#if HTS_FAST_CACHE
if (hash_pos_return) { // OK existe en cache (et données aussi)!
#else
if (a != NULL) { // OK existe en cache (et données aussi)!
#endif
if (hash_pos_return) { // in cache, with data
const int cache_is_prioritary = cache->type == 1
|| opt->state.stop != 0;
if (cache_is_prioritary) { // cache prioritaire (pas de test if-modified..)
@@ -1898,7 +1906,8 @@ int back_add(struct_back * sback, httrackp * opt, cache_back * cache, const char
#endif
}
}
/* Not in cache ; maybe in temporary cache ? Warning: non-movable "url_sav" */
/* Not in cache ; maybe in temporary cache ? Warning: non-movable
"url_sav" */
else if (back_unserialize_ref(opt, adr, fil, &itemback) == 0) {
const off_t file_size = fsize_utf8(itemback->url_sav);
@@ -2083,13 +2092,9 @@ int back_add(struct_back * sback, httrackp * opt, cache_back * cache, const char
if (!back_trylive(opt, cache, sback, p)) {
#if HTS_XGETHOST
#if HDEBUG
printf("back_solve..\n");
#endif
back[p].status = STATUS_WAIT_DNS; // tentative de résolution du nom de host
soc = INVALID_SOCKET; // pas encore ouverte
back_solve(opt, &back[p]); // préparer
if (host_wait(opt, &back[p])) { // prêt, par ex fichier ou dispo dans dns
back[p].status = STATUS_WAIT_DNS; // host name resolution attempt
soc = INVALID_SOCKET; // not opened yet
if (host_wait(opt, &back[p])) { // ready (file, or cached dns)
#if HDEBUG
printf("ok, dns cache ready..\n");
#endif
@@ -2232,37 +2237,9 @@ int back_add(struct_back * sback, httrackp * opt, cache_back * cache, const char
}
#if HTS_XGETHOST
// attendre que le host (ou celui du proxy) ait été résolu
// si c'est un fichier, la résolution est immédiate
// idem pour ftp://
void back_solve(httrackp * opt, lien_back * back) {
assertf(opt != NULL);
assertf(back != NULL);
if ((!strfield(back->url_adr, "file://"))
&& !strfield(back->url_adr, "ftp://")
) {
const char *a;
if (!(back->r.req.proxy.active))
a = back->url_adr;
else
a = back->r.req.proxy.name;
assertf(a != NULL);
a = jump_protocol_const(a);
if (check_hostname_dns(a)) {
hts_log_print(opt, LOG_DEBUG, "resolved: %s", a);
} else {
hts_log_print(opt, LOG_DEBUG, "failed to resolve: %s", a);
}
}
}
// détermine si le host a pu être résolu
int host_wait(httrackp * opt, lien_back * back) {
// Always synchronous. No more background DNS resolution
// (does not really improve performances)
return 1;
}
// Resolution is synchronous inside the connect path; no pre-resolve step, so
// the host is always immediately ready.
int host_wait(httrackp *opt, lien_back *back) { return 1; }
#endif
// élimine les fichiers non html en backing (anticipation)
@@ -2449,9 +2426,15 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
back_clean(opt, cache, sback);
#endif
/* Time limit exceeded past grace: abort in-flight transfers so no wait loop
starves (#481). FTP slots stay, their thread owns the socket. */
/* Time/size limit exceeded past grace: abort in-flight transfers so no wait
loop starves (#481, #77). FTP slots stay, their thread owns the socket. */
if (!back_checkmirror(opt)) {
const hts_mirror_limit limit = back_mirror_limit(opt);
const char *const reason =
(limit == HTS_MIRROR_LIMIT_SIZE) ? "size limit" : "time limit";
const char *const slotmsg = (limit == HTS_MIRROR_LIMIT_SIZE)
? "Mirror Size Limit"
: "Mirror Time Out";
int aborted = 0;
unsigned int i;
@@ -2465,15 +2448,15 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
if (back[i].r.is_write && IS_DELAYED_EXT(back[i].url_sav))
back_delayed_discard(opt, &back[i]);
back[i].r.statuscode = STATUSCODE_TIMEOUT;
strcpybuff(back[i].r.msg, "Mirror Time Out");
strcpybuff(back[i].r.msg, slotmsg);
back[i].status = STATUS_READY;
back_set_finished(sback, i);
aborted++;
}
}
if (aborted > 0)
hts_log_print(opt, LOG_WARNING,
"time limit reached, %d transfer(s) aborted", aborted);
hts_log_print(opt, LOG_WARNING, "%s reached, %d transfer(s) aborted",
reason, aborted);
return;
}
@@ -2963,7 +2946,8 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
strfield(get_ext(catbuff, sizeof(catbuff), back[i].url_sav), "gz") == 0
&& strfield(get_ext(catbuff, sizeof(catbuff), back[i].url_sav), "tgz") == 0
) {
if (create_back_tmpfile(opt, &back[i]) == 0) {
if (create_back_tmpfile(opt, &back[i], "z") ==
0) {
assertf(back[i].tmpfile != NULL);
/* note: tmpfile is utf-8 */
if ((back[i].r.out =
@@ -2976,6 +2960,20 @@ 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;
}
}
if ((back[i].r.out =
filecreate(&opt->state.strc,
back[i].url_sav)) == NULL) {
@@ -3542,22 +3540,20 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
if (back[i].r.statuscode == 406) { // 'Not Acceptable'
back[i].r.statuscode = HTTP_OK;
}
// 'do not erase already downloaded file'
// on an updated file
// with an error : consider a 304 error
if (!opt->delete_old) {
if (HTTP_IS_ERROR(back[i].r.statuscode) && back[i].is_update
&& !back[i].testmode) {
if (back[i].url_sav[0] && fexist_utf8(back[i].url_sav)) {
hts_log_print(opt, LOG_DEBUG,
"Error ignored %d (%s) because of 'no purge' option for %s%s",
back[i].r.statuscode, back[i].r.msg,
back[i].url_adr, back[i].url_fil);
back[i].r.statuscode = HTTP_NOT_MODIFIED;
deletehttp(&back[i].r);
back[i].r.soc = INVALID_SOCKET;
}
}
// On update, keep the good copy on error (mask as 304); skip
// resume paths so a stale-partial 416 still re-fetches.
if (HTTP_IS_ERROR(back[i].r.statuscode) &&
back[i].is_update && !back[i].testmode &&
back[i].range_req_size == 0 && back[i].url_sav[0] &&
fexist_utf8(back[i].url_sav)) {
hts_log_print(opt, LOG_DEBUG,
"Error %d (%s) ignored on update, keeping "
"existing copy: %s%s",
back[i].r.statuscode, back[i].r.msg,
back[i].url_adr, back[i].url_fil);
back[i].r.statuscode = HTTP_NOT_MODIFIED;
deletehttp(&back[i].r);
back[i].r.soc = INVALID_SOCKET;
}
// Various hacks to limit re-transfers when updating a mirror
// Force update if same size detected
@@ -4178,38 +4174,43 @@ static int back_maxtime_grace(const int maxtime) {
return maximum(5, minimum(30, maxtime / 10));
}
int back_checkmirror(httrackp * opt) {
// Check max size
if ((opt->maxsite > 0) && (HTS_STAT.stat_bytes >= opt->maxsite)) {
if (!opt->state.stop) { /* not yet stopped */
hts_log_print(opt, LOG_ERROR,
"More than " LLintP
" bytes have been transferred.. giving up",
(LLint) opt->maxsite);
/* cancel mirror smoothly */
hts_request_stop(opt, 0);
}
return 1; /* don'k break mirror too sharply for size limits, but stop requested */
/*return 0;
*/
}
// Check max time
/* Bytes the smooth stop may overrun before in-flight transfers are aborted.
No floor (unlike the time grace): a size overrun should abort promptly. */
static LLint back_maxsize_grace(const LLint maxsite) { return maxsite / 10; }
/* Which cap has overrun its grace and must hard-stop the mirror (#77, #481). */
static hts_mirror_limit back_mirror_limit(httrackp *opt) {
if (opt->maxsite > 0 && HTS_STAT.stat_bytes >= opt->maxsite &&
HTS_STAT.stat_bytes - opt->maxsite >= back_maxsize_grace(opt->maxsite))
return HTS_MIRROR_LIMIT_SIZE;
if (opt->maxtime > 0) {
const TStamp elapsed = time_local() - HTS_STAT.stat_timestart;
if (elapsed >= opt->maxtime) {
if (!opt->state.stop) { /* not yet stopped */
hts_log_print(opt, LOG_ERROR, "More than %d seconds passed.. giving up",
opt->maxtime);
/* cancel mirror smoothly */
hts_request_stop(opt, 0);
}
/* smooth stop starved past the grace period: stop waiting (#481) */
if (elapsed - opt->maxtime >= back_maxtime_grace(opt->maxtime))
return 0;
}
if (elapsed >= opt->maxtime &&
elapsed - opt->maxtime >= back_maxtime_grace(opt->maxtime))
return HTS_MIRROR_LIMIT_TIME;
}
return 1; /* Ok, go on */
return HTS_MIRROR_LIMIT_NONE;
}
int back_checkmirror(httrackp *opt) {
/* request a smooth stop the first time each cap is reached */
if (opt->maxsite > 0 && HTS_STAT.stat_bytes >= opt->maxsite &&
!opt->state.stop) {
hts_log_print(opt, LOG_ERROR,
"More than " LLintP
" bytes have been transferred.. giving up",
(LLint) opt->maxsite);
hts_request_stop(opt, 0);
}
if (opt->maxtime > 0 && !opt->state.stop &&
(time_local() - HTS_STAT.stat_timestart) >= opt->maxtime) {
hts_log_print(opt, LOG_ERROR, "More than %d seconds passed.. giving up",
opt->maxtime);
hts_request_stop(opt, 0);
}
/* hard stop once a cap overruns its grace (callers must stop waiting) */
return back_mirror_limit(opt) == HTS_MIRROR_LIMIT_NONE;
}
// octets transférés + add

View File

@@ -138,12 +138,11 @@ LLint back_transferred(LLint add, struct_back * sback);
// hostback
#if HTS_XGETHOST
void back_solve(httrackp * opt, lien_back * sback);
int host_wait(httrackp * opt, lien_back * sback);
#endif
int back_checksize(httrackp * opt, lien_back * eback, int check_only_totalsize);
/* Enforce -M/-E quotas: requests a smooth stop when reached; returns 0 once
the -E deadline overran its grace period (callers must stop waiting). */
/* Enforce -M/-E quotas: smooth-stops when reached; returns 0 once the -M cap
or -E deadline overruns its grace period (callers must stop waiting). */
int back_checkmirror(httrackp * opt);
#endif

View File

@@ -307,14 +307,25 @@ int cookie_load(t_cookie * cookie, const char *fpath, const char *name) {
return -1;
}
// écrire cookies.txt
// !=0 : erreur
/* Write cookies.txt; returns 0 on success. The jar holds live session
cookies, so keep it owner-only on Unix (Windows inherits folder ACLs). */
int cookie_save(t_cookie * cookie, const char *name) {
char catbuff[CATBUFF_SIZE];
if (strnotempty(cookie->data)) {
char BIGSTK line[8192];
#ifdef _WIN32
FILE *fp = fopen(fconv(catbuff, sizeof(catbuff), name), "wb");
#else
const int fd = open(fconv(catbuff, sizeof(catbuff), name),
O_WRONLY | O_CREAT | O_TRUNC, HTS_PROTECT_FILE);
FILE *fp = fd != -1 ? fdopen(fd, "wb") : NULL;
if (fd != -1 && fp == NULL)
close(fd); /* fdopen failed: don't leak the descriptor */
if (fp != NULL) /* O_CREAT's mode skips pre-existing jars: tighten those */
(void) fchmod(fd, HTS_PROTECT_FILE);
#endif
if (fp) {
char *a = cookie->data;

File diff suppressed because it is too large Load Diff

View File

@@ -89,14 +89,11 @@ typedef enum {
the involved files are absent. */
void hts_cache_reconcile(httrackp *opt, hts_cache_reconcile_mode mode);
int cache_writedata(FILE * cache_ndx, FILE * cache_dat, const char *str1,
const char *str2, char *outbuff, int len);
int cache_readdata(cache_back * cache, const char *str1, const char *str2,
char **inbuff, int *len);
void cache_rstr(FILE *fp, char *s, size_t s_size);
char *cache_rstr_addr(FILE * fp);
int cache_brstr(char *adr, char *s, size_t s_size);
/* binput over a NUL-terminated buffer, bounded: no read starts at/past end. */
int cache_binput(char *adr, const char *end, char *s, int max);
int cache_brint(char *adr, int *i);
void cache_rint(FILE * fp, int *i);
void cache_rLLint(FILE * fp, LLint * i);

View File

@@ -78,14 +78,6 @@ static void selftest_open_for_read(cache_back *cache, httrackp *opt) {
}
static void selftest_close(cache_back *cache) {
if (cache->dat != NULL) {
fclose(cache->dat);
cache->dat = NULL;
}
if (cache->ndx != NULL) {
fclose(cache->ndx);
cache->ndx = NULL;
}
if (cache->zipOutput != NULL) {
zipClose(cache->zipOutput,
"Created by HTTrack Website Copier (cache self-test)");
@@ -989,26 +981,15 @@ int cache_reconcile_selftest(httrackp *opt, const char *dir) {
failures +=
reconcile_expect(opt, "hts-cache/old.zip", SOLID, "promote-zip-noop");
/* PROMOTE: a pure-legacy old generation is promoted too (was dead when no
zip cache existed) */
/* PROMOTE: the removed pre-3.31 legacy pair is left alone */
reconcile_wipe(opt);
reconcile_put(opt, "hts-cache/old.dat", SOLID);
reconcile_put(opt, "hts-cache/old.ndx", TINY);
hts_cache_reconcile(opt, CACHE_RECONCILE_PROMOTE);
failures += reconcile_expect(opt, "hts-cache/new.dat", SOLID, "promote-dat");
failures += reconcile_expect(opt, "hts-cache/new.ndx", TINY, "promote-dat");
failures += reconcile_expect(opt, "hts-cache/old.dat", -1, "promote-dat");
/* PROMOTE: a half-written legacy new pair is replaced by the old pair */
reconcile_wipe(opt);
reconcile_put(opt, "hts-cache/new.dat", TINY);
reconcile_put(opt, "hts-cache/old.dat", SOLID);
reconcile_put(opt, "hts-cache/old.ndx", TINY);
hts_cache_reconcile(opt, CACHE_RECONCILE_PROMOTE);
failures +=
reconcile_expect(opt, "hts-cache/new.dat", SOLID, "promote-dat-partial");
failures +=
reconcile_expect(opt, "hts-cache/new.ndx", TINY, "promote-dat-partial");
failures += reconcile_expect(opt, "hts-cache/new.dat", -1, "promote-dat");
failures += reconcile_expect(opt, "hts-cache/new.ndx", -1, "promote-dat");
failures += reconcile_expect(opt, "hts-cache/old.dat", SOLID, "promote-dat");
failures += reconcile_expect(opt, "hts-cache/old.ndx", TINY, "promote-dat");
/* INTERRUPTED: no lock file, no action */
reconcile_wipe(opt);
@@ -1058,7 +1039,7 @@ int cache_reconcile_selftest(httrackp *opt, const char *dir) {
failures +=
reconcile_expect(opt, "hts-cache/new.zip", MID, "interrupted-bignew");
/* INTERRUPTED: the legacy pair follows the same size rule (was dead code) */
/* INTERRUPTED: the removed pre-3.31 legacy pair is left alone */
reconcile_wipe(opt);
reconcile_put(opt, "hts-in_progress.lock", 0);
reconcile_put(opt, "hts-cache/new.dat", TINY);
@@ -1067,9 +1048,13 @@ int cache_reconcile_selftest(httrackp *opt, const char *dir) {
reconcile_put(opt, "hts-cache/old.ndx", MID);
hts_cache_reconcile(opt, CACHE_RECONCILE_INTERRUPTED);
failures +=
reconcile_expect(opt, "hts-cache/new.dat", SOLID, "interrupted-dat");
reconcile_expect(opt, "hts-cache/new.dat", TINY, "interrupted-dat");
failures +=
reconcile_expect(opt, "hts-cache/new.ndx", MID, "interrupted-dat");
reconcile_expect(opt, "hts-cache/new.ndx", TINY, "interrupted-dat");
failures +=
reconcile_expect(opt, "hts-cache/old.dat", SOLID, "interrupted-dat");
failures +=
reconcile_expect(opt, "hts-cache/old.ndx", MID, "interrupted-dat");
/* ROLLBACK: the old zip generation is restored (a zip cache used to lose
its only good generation here) */
@@ -1089,17 +1074,18 @@ int cache_reconcile_selftest(httrackp *opt, const char *dir) {
failures += reconcile_expect(opt, "hts-cache/new.lst", MID, "rollback-lst");
failures += reconcile_expect(opt, "hts-cache/new.txt", MID, "rollback-txt");
/* ROLLBACK: full legacy generation incl. sidecars (historical behavior) */
/* ROLLBACK: sidecars restored, the removed pre-3.31 pair left alone */
reconcile_wipe(opt);
reconcile_put(opt, "hts-cache/new.dat", TINY);
reconcile_put(opt, "hts-cache/new.ndx", TINY);
reconcile_put(opt, "hts-cache/old.dat", SOLID);
reconcile_put(opt, "hts-cache/old.ndx", MID);
reconcile_put(opt, "hts-cache/old.lst", MID);
reconcile_put(opt, "hts-cache/old.txt", MID);
hts_cache_reconcile(opt, CACHE_RECONCILE_ROLLBACK);
failures += reconcile_expect(opt, "hts-cache/new.dat", SOLID, "rollback-dat");
failures += reconcile_expect(opt, "hts-cache/new.ndx", MID, "rollback-dat");
failures += reconcile_expect(opt, "hts-cache/new.dat", TINY, "rollback-dat");
failures += reconcile_expect(opt, "hts-cache/new.ndx", -1, "rollback-dat");
failures += reconcile_expect(opt, "hts-cache/old.dat", SOLID, "rollback-dat");
failures += reconcile_expect(opt, "hts-cache/old.ndx", MID, "rollback-dat");
failures += reconcile_expect(opt, "hts-cache/new.lst", MID, "rollback-dat");
failures += reconcile_expect(opt, "hts-cache/new.txt", MID, "rollback-dat");
@@ -1113,6 +1099,101 @@ int cache_reconcile_selftest(httrackp *opt, const char *dir) {
return failures;
}
/* The pre-3.31 .dat/.ndx import was removed: cache_init must refuse the
legacy pair, leave the files in place, and not flag an update run. */
int cache_legacy_refused_selftest(httrackp *opt, const char *dir) {
int failures = 0;
int variant;
selftest_tag = "cache-legacy";
golden_setup(opt, dir);
#ifdef _WIN32
mkdir(reconcile_st_path(opt, "hts-cache"));
#else
mkdir(reconcile_st_path(opt, "hts-cache"), HTS_PROTECT_FOLDER);
#endif
/* variant 0: old.dat/old.ndx (--update layout); 1: new.dat/new.ndx (ro) */
for (variant = 0; variant < 2; variant++) {
cache_back cache;
const char *const dat =
variant == 0 ? "hts-cache/old.dat" : "hts-cache/new.dat";
const char *const ndx =
variant == 0 ? "hts-cache/old.ndx" : "hts-cache/new.ndx";
reconcile_wipe(opt);
reconcile_put(opt, dat, 4096);
reconcile_put(opt, ndx, 4096);
opt->is_update = 0;
selftest_open(&cache, opt, /*ro*/ variant == 1);
if (cache_readable(&cache)) {
printf("cache-legacy: variant %d imported a removed format\n", variant);
failures++;
}
if (coucal_nitems(cache.hashtable) != 0) { /* no phantom index entries */
printf("cache-legacy: variant %d imported index entries\n", variant);
failures++;
}
if (opt->is_update) {
printf("cache-legacy: variant %d flagged an update\n", variant);
failures++;
}
if (fsize(reconcile_st_path(opt, dat)) != 4096 ||
fsize(reconcile_st_path(opt, ndx)) != 4096) {
printf("cache-legacy: variant %d touched the legacy files\n", variant);
failures++;
}
if (cache.lst != NULL) {
fclose(cache.lst);
cache.lst = NULL;
}
if (cache.txt != NULL) {
fclose(cache.txt);
cache.txt = NULL;
}
selftest_close(&cache);
}
/* a healthy zip must win over stray legacy files, with no refusal */
{
cache_back cache;
const char *const body = "<html>zip wins</html>";
reconcile_wipe(opt);
selftest_open_for_write(&cache, opt);
store_entry(opt, &cache, "example.com", "/", "example.com/index.html", 200,
"OK", "text/html", "utf-8", "", "", "", "", body, strlen(body));
selftest_close(&cache);
reconcile_put(opt, "hts-cache/old.ndx", 4096);
reconcile_put(opt, "hts-cache/old.dat", 4096);
selftest_open_for_read(&cache, opt);
if (!cache_readable(&cache)) {
printf("cache-legacy: stray legacy files shadowed the zip cache\n");
failures++;
}
failures +=
check_entry(opt, &cache, "example.com", "/", 200, "OK", "text/html",
"utf-8", "", "", "", "", body, strlen(body));
if (fsize(reconcile_st_path(opt, "hts-cache/old.ndx")) != 4096 ||
fsize(reconcile_st_path(opt, "hts-cache/old.dat")) != 4096) {
printf("cache-legacy: zip-wins pass touched the legacy files\n");
failures++;
}
if (cache.lst != NULL) {
fclose(cache.lst);
cache.lst = NULL;
}
if (cache.txt != NULL) {
fclose(cache.txt);
cache.txt = NULL;
}
selftest_close(&cache);
}
reconcile_wipe(opt);
return failures;
}
/* --- read-side corruption injection --------------------------------------- */
/* canary read back intact after each corruption; victim gets the byte surgery

View File

@@ -60,6 +60,10 @@ int cache_write_failure_selftest(httrackp *opt, const char *dir);
under <dir>. Returns the failed-check count. */
int cache_reconcile_selftest(httrackp *opt, const char *dir);
/* Verify cache_init refuses a pre-3.31 .dat/.ndx cache without touching it.
Returns the number of failed checks (0 = pass). */
int cache_legacy_refused_selftest(httrackp *opt, const char *dir);
/* Inject read-side corruption (zip byte surgery: bad size, header, deflate)
under <dir> and assert every case degrades to STATUSCODE_INVALID without
tainting a sibling entry. */

View File

@@ -84,7 +84,7 @@ static int hts_equalsAlphanum(const char *a, const char *b) {
/* Copy the memory region [s .. s + size - 1 ] as a \0-terminated string. */
static char *hts_stringMemCopy(const char *s, size_t size) {
char *dest = malloc(size + 1);
char *dest = malloct(size + 1);
if (dest != NULL) {
memcpy(dest, s, size);
@@ -549,42 +549,6 @@ char *hts_convertStringFromUTF8(const char *s, size_t size, const char *charset)
#endif
HTS_STATIC char *hts_getCharsetFromContentType(const char *mime) {
/* text/html; charset=utf-8 */
const char *const charset = "charset";
char *pos = strstr(mime, charset);
if (pos != NULL) {
/* Skip spaces */
int eq = 0;
for(pos += strlen(charset);
*pos == ' ' || *pos == '=' || *pos == '"' || *pos == '\''; pos++) {
if (*pos == '=') {
eq = 1;
}
}
if (eq == 1) {
int len;
for(len = 0;
pos[len] == ' ' || pos[len] == ';' || pos[len] == '"' || *pos == '\'';
pos++) ;
if (len != 0) {
char *const s = malloc(len + 1);
int i;
for(i = 0; i < len; i++) {
s[i] = pos[i];
}
s[len] = '\0';
return s;
}
}
}
return NULL;
}
#ifdef _WIN32
#define strcasecmp(a,b) stricmp(a,b)
#define strncasecmp(a,b,n) strnicmp(a,b,n)
@@ -644,58 +608,106 @@ int hts_isCharsetUTF8(const char *charset) {
|| strcasecmp(charset, "utf8") == 0 );
}
/* Extract X from a "text/html; charset=X" content= attribute value. */
static char *charset_from_content(const char *s, size_t len) {
size_t i;
for (i = 0; i + 7 < len; i++) {
if ((i == 0 || is_space(s[i - 1]) || s[i - 1] == ';') &&
strncasecmp(&s[i], "charset", 7) == 0 && is_space_or_equal(s[i + 7])) {
size_t j, val;
for (j = i + 7; j < len && is_space_or_equal_or_quote(s[j]); j++)
;
for (val = j; j < len && s[j] != '"' && s[j] != '\'' && s[j] != ';' &&
!is_space(s[j]);
j++)
;
if (j != val) {
return hts_stringMemCopy(&s[val], j - val);
}
}
}
return NULL;
}
char *hts_getCharsetFromMeta(const char *html, size_t size) {
int i;
size_t i;
/* <META HTTP-EQUIV="CONTENT-TYPE" CONTENT="text/html; charset=utf-8" > */
for(i = 0; i < size; i++) {
if (html[i] == '<' && strncasecmp(&html[i + 1], "meta", 4) == 0
&& is_space(html[i + 5])) {
/* Skip spaces */
for(i += 5; is_space(html[i]); i++) ;
if (strncasecmp(&html[i], "HTTP-EQUIV", 10) == 0
&& is_space_or_equal(html[i + 10])) {
for(i += 10; is_space_or_equal_or_quote(html[i]); i++) ;
if (strncasecmp(&html[i], "CONTENT-TYPE", 12) == 0) {
for(i += 12; is_space_or_equal_or_quote(html[i]); i++) ;
if (strncasecmp(&html[i], "CONTENT", 7) == 0
&& is_space_or_equal(html[i + 7])) {
for(i += 7; is_space_or_equal_or_quote(html[i]); i++) ;
/* Skip content-type */
for(;
i < size && html[i] != ';' && html[i] != '"' && html[i] != '\'';
i++) ;
/* Expect charset attribute here */
if (html[i] == ';') {
for(i++; is_space(html[i]); i++) ;
/* Look for charset */
if (strncasecmp(&html[i], "charset", 7) == 0
&& is_space_or_equal(html[i + 7])) {
int len;
/* HTML5 <meta charset=X> or legacy
<meta http-equiv="Content-Type" content="text/html; charset=X">,
attributes in any order; first resolvable tag wins. */
for (i = 0; i + 6 < size; i++) {
size_t j;
const char *equiv = NULL, *content = NULL;
size_t equiv_len = 0, content_len = 0;
for(i += 7; is_space_or_equal(html[i]) || html[i] == '\'';
i++) ;
/* Charset */
for(len = 0;
i + len < size && html[i + len] != '"'
&& html[i + len] != '\'' && html[i + len] != ' '; len++) ;
/* No error ? */
if (len != 0 && i < size) {
char *const s = malloc(len + 1);
int j;
if (html[i] != '<' || strncasecmp(&html[i + 1], "meta", 4) != 0 ||
!is_space(html[i + 5])) {
continue;
}
/* Attribute scan, strictly bounded by size. */
for (j = i + 6; j < size && html[j] != '>';) {
size_t name, name_len, val = 0, val_len = 0;
for(j = 0; j < len; j++) {
s[j] = html[i + j];
}
s[len] = '\0';
return s;
}
}
}
if (is_space(html[j]) || html[j] == '/') {
j++;
continue;
}
for (name = j; j < size && html[j] != '=' && html[j] != '>' &&
html[j] != '/' && !is_space(html[j]);
j++)
;
name_len = j - name;
for (; j < size && is_space(html[j]); j++)
;
if (j < size && html[j] == '=') {
for (j++; j < size && is_space(html[j]); j++)
;
if (j < size && (html[j] == '"' || html[j] == '\'')) {
const char quote = html[j++];
for (val = j; j < size && html[j] != quote; j++)
;
if (j >= size) /* unterminated quote: drop the tag */
break;
val_len = j++ - val;
} else {
/* unquoted: ends at whitespace or '>' only (HTML5 prescan); '/'
belongs to the value except as the tail of a self-close */
for (val = j; j < size && !is_space(html[j]) && html[j] != '>'; j++)
;
val_len = j - val;
if (val_len != 0 && j < size && html[j] == '>' &&
html[val + val_len - 1] == '/') {
val_len--;
}
}
}
/* first occurrence of each attribute wins, as in browsers */
if (val_len != 0) {
if (name_len == 7 && strncasecmp(&html[name], "charset", 7) == 0) {
return hts_stringMemCopy(&html[val], val_len);
} else if (equiv == NULL && name_len == 10 &&
strncasecmp(&html[name], "http-equiv", 10) == 0) {
equiv = &html[val];
equiv_len = val_len;
} else if (content == NULL && name_len == 7 &&
strncasecmp(&html[name], "content", 7) == 0) {
content = &html[val];
content_len = val_len;
}
}
}
if (equiv_len == 12 && strncasecmp(equiv, "content-type", 12) == 0 &&
content != NULL) {
char *const s = charset_from_content(content, content_len);
if (s != NULL) {
return s;
}
}
i = j;
}
return NULL;
}
@@ -1140,17 +1152,45 @@ int hts_isStringUTF8(const char *s, size_t size) {
const unsigned char *const data = (const unsigned char*) s;
size_t i;
/* Strict RFC 3629: reject overlongs, surrogates, > U+10FFFF, 5/6-byte. */
for(i = 0 ; i < size ; ) {
/* Reader: can read bytes up to j */
#define RD ( i < size ? data[i++] : -1 )
const unsigned char c = data[i];
size_t len, k;
hts_UCS4 uc, min;
/* Writer: a malformed sequence means the string is not UTF-8 (return 0) */
#define WR(C) if ((C) == -1) { return 0; }
if (c < 0x80) {
i++;
continue;
} else if (c >= 0xc2 && c <= 0xdf) {
len = 2;
min = 0x80;
uc = c & 0x1f;
} else if (c >= 0xe0 && c <= 0xef) {
len = 3;
min = 0x800;
uc = c & 0x0f;
} else if (c >= 0xf0 && c <= 0xf4) {
len = 4;
min = 0x10000;
uc = c & 0x07;
} else { /* continuation byte, overlong C0/C1, or F5..FF lead */
return 0;
}
if (size - i < len) {
return 0;
}
for (k = 1; k < len; k++) {
const unsigned char cc = data[i + k];
/* Read Unicode character. */
READ_UNICODE(RD, WR);
#undef RD
#undef WR
if ((cc & 0xc0) != 0x80) {
return 0;
}
uc = (uc << 6) | (cc & 0x3f);
}
if (uc < min || uc > 0x10FFFF || (uc >= 0xD800 && uc <= 0xDFFF)) {
return 0;
}
i += len;
}
return 1;

View File

@@ -76,7 +76,8 @@ extern char *hts_convertStringIDNAToUTF8(const char *s, size_t size);
extern int hts_isStringIDNA(const char *s, size_t size);
/**
* Extract the charset from the HTML buffer "html"
* Extract the <meta> charset from the HTML buffer "html" (HTML5 charset= or
* legacy http-equiv form). Returns a malloc'ed string, or NULL if none.
**/
extern char *hts_getCharsetFromMeta(const char *html, size_t size);
@@ -86,7 +87,8 @@ extern char *hts_getCharsetFromMeta(const char *html, size_t size);
extern int hts_isStringAscii(const char *s, size_t size);
/**
* Is the given string an UTF-8 string ?
* Is the given string valid UTF-8 ? Strict RFC 3629: overlong forms,
* surrogates, codepoints above U+10FFFF and 5/6-byte sequences are rejected.
**/
extern int hts_isStringUTF8(const char *s, size_t size);

View File

@@ -76,9 +76,6 @@ Please visit our Website: http://www.httrack.com
// always direct-to-disk (0/1)
#define HTS_DIRECTDISK_ALWAYS 1
// fast cache (build hash table)
#define HTS_FAST_CACHE 1
// le > peut être considéré comme un tag de fermeture de commentaire (<!-- > est
// valide)
#define GT_ENDS_COMMENT 1

View File

@@ -134,18 +134,6 @@ RUN_CALLBACK0(opt, end); \
back_delete_all(opt, &cache, sback); \
back_free(&sback); \
checkrobots_free(&robots); \
if (cache.use) { \
freet(cache.use); \
cache.use = NULL; \
} \
if (cache.dat) { \
fclose(cache.dat); \
cache.dat = NULL; \
} \
if (cache.ndx) { \
fclose(cache.ndx); \
cache.ndx = NULL; \
} \
if (cache.zipOutput) { \
zipClose(cache.zipOutput, \
"Created by HTTrack Website Copier/" HTTRACK_VERSION); \
@@ -155,10 +143,6 @@ RUN_CALLBACK0(opt, end); \
unzClose(cache.zipInput); \
cache.zipInput = NULL; \
} \
if (cache.olddat) { \
fclose(cache.olddat); \
cache.olddat = NULL; \
} \
if (cache.lst) { \
fclose(cache.lst); \
cache.lst = opt->state.strc.lst = NULL; \
@@ -488,32 +472,11 @@ void hts_finish_makeindex(httrackp *opt, int *makeindex_done,
*makeindex_done = 1;
}
/* Flush the parsed HTML output buffer to disk, skipping the rewrite when the
* on-disk MD5 is unchanged. */
/* Flush the parsed HTML output buffer to disk. */
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) {
char digest[32 + 2];
off_t fsize_old =
fsize(fconv(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt), save));
int ok = 0;
digest[0] = '\0';
domd5mem(ht_buff, ht_len, digest, 1);
if (fsize_old == (off_t) ht_len) {
int mlen = 0;
char *mbuff;
cache_readdata(cache, "//[HTML-MD5]//", save, &mbuff, &mlen);
if (mlen)
mbuff[mlen] = '\0';
if ((mlen == 32) && (strcmp(((mbuff != NULL) ? mbuff : ""), digest) == 0)) {
ok = 1;
hts_log_print(opt, LOG_DEBUG, "File not re-written (md5): %s", save);
}
freet(mbuff);
}
if (!ok) {
{
file_notify(opt, adr, fil, save, 1, 1, r->notmodified);
*fp = filecreate(&opt->state.strc, save);
if (*fp) {
@@ -545,13 +508,7 @@ void hts_finish_html_file(httrackp *opt, cache_back *cache, htsblk *r,
if (fcheck)
hts_log_print(opt, LOG_ERROR, "* * Fatal write error, giving up");
}
} else {
file_notify(opt, adr, fil, save, 0, 0, r->notmodified);
filenote(&opt->state.strc, save, NULL);
}
if (cache->ndx)
cache_writedata(cache->ndx, cache->dat, "//[HTML-MD5]//", save, digest,
(int) strlen(digest));
}
/* does it look like XML ? (SVG et al.) */
@@ -846,6 +803,52 @@ int httpmirror(char *url1, httrackp * opt) {
}
} // while
/* --why: print which filter rule decides for this URL, then stop */
if (StringNotEmpty(opt->why_url)) {
char BIGSTK url[HTS_URLMAXSIZE * 2];
lien_adrfil af;
if (strstr(StringBuff(opt->why_url), ":/") == NULL)
snprintf(url, sizeof(url), "http://%s", StringBuff(opt->why_url));
else
strcpybuff(url, StringBuff(opt->why_url));
if (ident_url_absolute(url, &af) < 0) {
printf("--why: unable to parse URL %s" LF, StringBuff(opt->why_url));
} else {
char BIGSTK l[HTS_URLMAXSIZE * 2], lfull[HTS_URLMAXSIZE * 2];
int jok, jokDepth = 0;
/* the two forms the wizard tests */
strcpybuff(l, jump_identification_const(af.adr));
if (*af.fil != '/')
strcatbuff(l, "/");
strcatbuff(l, af.fil);
if (!link_has_authority(af.adr))
strcpybuff(lfull, "http://");
else
lfull[0] = '\0';
strcatbuff(lfull, af.adr);
if (*af.fil != '/')
strcatbuff(lfull, "/");
strcatbuff(lfull, af.fil);
jok = fa_strjoker_dual(/*url */ 0, filters, filptr, lfull, l, NULL,
NULL, &jokDepth);
if (jok > 0)
printf("%s: accepted by rule #%d (%s)" LF, url, jokDepth + 1,
filters[jokDepth]);
else if (jok < 0)
printf("%s: rejected by rule #%d (%s)" LF, url, jokDepth + 1,
filters[jokDepth]);
else
printf("%s: no filter rule matches; the wizard decides "
"(same-site links are followed by default)" LF,
url);
}
freet(primary);
XH_extuninit;
return 1;
}
/* load URL file list */
/* OPTIMIZED for fast load */
if (StringNotEmpty(opt->filelist)) {
@@ -1901,8 +1904,11 @@ int httpmirror(char *url1, httrackp * opt) {
}
}
// ATTENTION C'EST ICI QU'ON SAUVE LE FICHIER!!
if (r.adr != NULL || r.size == 0) {
// ATTENTION C'EST ICI QU'ON SAUVE LE FICHIER!!
// An empty body must not overwrite the file when the transfer failed
// (statuscode <= 0, e.g. an -M hard-stop): it would truncate a good
// copy to 0 (#77 follow-up).
if (r.adr != NULL || (r.size == 0 && r.statuscode > 0)) {
file_notify(opt, urladr(), urlfil(), savename(), 1, 1, r.notmodified);
if (filesave(opt, r.adr, (int) r.size, savename(), urladr(), urlfil()) !=
0) {
@@ -1923,7 +1929,6 @@ int httpmirror(char *url1, httrackp * opt) {
*/
}
}
}
/* Parsing of other media types (java, ram..) */

View File

@@ -193,8 +193,7 @@ struct cache_back {
/* */
int type;
int ro; /**< read-only: no new cache is written */
FILE *dat, *ndx, *olddat; /**< new data, new index, old data files */
char *use; /**< in-memory list of cached adr+fil keys */
FILE *lst; /**< file list, used for purge */
FILE *txt; /**< human-readable file list (info) */
char lastmodified[256];
@@ -288,12 +287,12 @@ struct filecreate_params {
/* True if a new cache is being written (plain or zip backend). */
HTS_STATIC int cache_writable(cache_back * cache) {
return (cache != NULL && (cache->dat != NULL || cache->zipOutput != NULL));
return (cache != NULL && cache->zipOutput != NULL);
}
/* True if an old cache is available to read (plain or zip backend). */
HTS_STATIC int cache_readable(cache_back * cache) {
return (cache != NULL && (cache->olddat != NULL || cache->zipInput != NULL));
return (cache != NULL && cache->zipInput != NULL);
}
#endif

View File

@@ -1803,6 +1803,23 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
StringCopy(opt->cookies_file, argv[na]);
}
break;
case 'Y': // why: explain the filter verdict for a URL, no crawl
if ((na + 1 >= argc) || (argv[na + 1][0] == '-')) {
HTS_PANIC_PRINTF("Option why needs a blank space and a URL");
printf(
"Example: --why \"http://www.example.com/file.zip\"\n");
htsmain_free();
return -1;
} else {
na++;
if (strlen(argv[na]) >= HTS_URLMAXSIZE) {
HTS_PANIC_PRINTF("why URL too long");
htsmain_free();
return -1;
}
StringCopy(opt->why_url, argv[na]);
}
break;
case 'G': // pause: randomized inter-file delay MIN[:MAX] seconds
if ((na + 1 >= argc) || (argv[na + 1][0] == '-')) {
HTS_PANIC_PRINTF("Option pause needs a blank space and a "
@@ -1936,18 +1953,20 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
char BIGSTK url[HTS_URLMAXSIZE * 2];
char linepos[256];
int pos;
char *cacheNdx =
readfile(fconcat
(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt), StringBuff(opt->path_log),
"hts-cache/new.ndx"));
LLint cacheNdxLen = 0;
char *cacheNdx = readfile2(
fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
StringBuff(opt->path_log), "hts-cache/new.ndx"),
&cacheNdxLen);
cache_init(&cache, opt); /* load cache */
if (cacheNdx != NULL) {
char firstline[256];
char *a = cacheNdx;
const char *const end = cacheNdx + cacheNdxLen;
a += cache_brstr(a, firstline, sizeof(firstline));
a += cache_brstr(a, firstline, sizeof(firstline));
while(a != NULL) {
while (a != NULL && a < end) {
a = strchr(a + 1, '\n'); /* start of line */
if (a) {
htsblk r;
@@ -1955,15 +1974,15 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
/* */
a++;
/* read "host/file" */
a += binput(a, afs.af.adr, HTS_URLMAXSIZE);
a += binput(a, afs.af.fil, HTS_URLMAXSIZE);
a += cache_binput(a, end, afs.af.adr, HTS_URLMAXSIZE);
a += cache_binput(a, end, afs.af.fil, HTS_URLMAXSIZE);
url[0] = '\0';
if (!link_has_authority(afs.af.adr))
strcatbuff(url, "http://");
strcatbuff(url, afs.af.adr);
strcatbuff(url, afs.af.fil);
/* read position */
a += binput(a, linepos, 200);
a += cache_binput(a, end, linepos, 200);
sscanf(linepos, "%d", &pos);
if (!hasFilter
|| (strjoker(url, filter, NULL, NULL) != NULL)

View File

@@ -297,6 +297,26 @@ int dns_selftests(httrackp *opt) {
IPV6_resolver = 0;
}
/* "host:port" resolves as the bare host: the cache/connect path strips
":port" before both the backend and the cache key (#181). */
mock_reset_calls();
{
SOCaddr a;
char ip[64];
const char *err = NULL;
/* cache miss: the backend is hit with the stripped host, not "host:port" */
CHECK(hts_dns_resolve2(opt, "v6only.test:8080", &a, &err) != NULL);
CHECK(mock_find("v6only.test")->calls == 1);
/* the bare host shares that one cache entry (stripped key) */
CHECK(hts_dns_resolve2(opt, "v6only.test", &a, &err) != NULL);
CHECK(mock_find("v6only.test")->calls == 1);
/* a port variant of an already-cached host also hits, right address */
CHECK(hts_dns_resolve2(opt, "v4only.test:1234", &a, &err) != NULL);
SOCaddr_inetntoa(ip, sizeof(ip), a);
CHECK(strcmp(ip, "1.2.3.4") == 0);
}
/* newhttp_addr() must connect to the addr_index-th address, not always the
first: this is what back_connect_next relies on to reach the fallback. */
{

View File

@@ -48,6 +48,7 @@ Please visit our Website: http://www.httrack.com
#include "htsbase.h"
#include "htslib.h"
#include <ctype.h>
#include <stdint.h>
/* END specific definitions */
// à partir d'un tableau de {"+*.toto","-*.zip","+*.tata"} définit si nom est autorisé
@@ -94,16 +95,107 @@ int fa_strjoker(int type, char **filters, int nfil, const char *nom, LLint * siz
return verdict;
}
// supercomparateur joker (tm)
// compare a et b (b=avec joker dedans), case insensitive [voir CI]
// renvoi l'adresse de la première lettre de la chaine
// (càd *[..]toto.. renvoi adresse de toto dans la chaine)
// accepte les délires du genre www.*.*/ * / * truc*.*
// cet algo est 'un peu' récursif mais ne consomme pas trop de tm
// * = toute lettre
// --?-- : spécifique à HTTrack et aux ?
int fa_strjoker_dual(int type, char **filters, int nfil, const char *nom1,
const char *nom2, LLint *size, int *size_flag,
int *depth) {
int depth1 = 0, depth2 = 0;
int flag1 = 0, flag2 = 0;
LLint sz1 = 0, sz2 = 0;
int jok1, jok2, use1;
if (size) {
sz1 = *size;
sz2 = *size;
}
jok1 = fa_strjoker(type, filters, nfil, nom1, size ? &sz1 : NULL,
size_flag ? &flag1 : NULL, &depth1);
jok2 = fa_strjoker(type, filters, nfil, nom2, size ? &sz2 : NULL,
size_flag ? &flag2 : NULL, &depth2);
use1 = jok2 == 0 || (jok1 != 0 && depth1 >= depth2);
if (size)
*size = use1 ? sz1 : sz2;
if (size_flag)
*size_flag = use1 ? flag1 : flag2;
if (depth)
*depth = use1 ? depth1 : depth2;
return use1 ? jok1 : jok2;
}
/* Failure memo for the recursive matcher: one bit per (chaine, joker) offset
pair keeps star-heavy patterns polynomial instead of exponential (#501). */
typedef struct strjoker_memo {
const char *chaine0, *joker0; /* offsets are relative to these bases */
size_t stride; /* strlen(joker0) + 1 */
unsigned char *failed; /* failed-pair bitmap; NULL: no memo */
} strjoker_memo;
static const char *strjoker_impl(const strjoker_memo *memo, const char *chaine,
const char *joker, LLint *size,
int *size_flag);
/* A pair that failed once fails forever (*size is only written on the success
path), so record NULL results and cut the re-exploration. */
static const char *strjoker_rec(const strjoker_memo *memo, const char *chaine,
const char *joker, LLint *size,
int *size_flag) {
size_t bit = 0;
const char *adr;
if (memo->failed) {
bit = (size_t) (chaine - memo->chaine0) * memo->stride +
(size_t) (joker - memo->joker0);
if (memo->failed[bit >> 3] & (unsigned char) (1u << (bit & 7)))
return NULL;
}
adr = strjoker_impl(memo, chaine, joker, size, size_flag);
if (adr == NULL && memo->failed)
memo->failed[bit >> 3] |= (unsigned char) (1u << (bit & 7));
return adr;
}
// wildcard comparator: match chaine against joker (pattern), case-insensitive
// returns the address of the first matched letter past any leading joker
// (ie. *[..]toto.. returns the address of toto within chaine), NULL on mismatch
HTS_INLINE const char *strjoker(const char *chaine, const char *joker,
LLint *size, int *size_flag) {
strjoker_memo memo = {chaine, joker, 0, NULL};
unsigned char stackbits[2048];
hts_boolean onheap = HTS_FALSE;
const char *adr;
if (chaine != NULL && joker != NULL) {
const size_t n1 = strlen(chaine) + 1;
memo.stride = strlen(joker) + 1;
if (n1 <= (SIZE_MAX - 7) / memo.stride) { // overflow-safe bitmap size
const size_t bytes = (n1 * memo.stride + 7) / 8;
if (bytes <= sizeof(stackbits)) {
memset(stackbits, 0, bytes);
memo.failed = stackbits;
} else {
memo.failed = (unsigned char *) calloct(bytes, 1);
onheap = memo.failed != NULL ? HTS_TRUE : HTS_FALSE;
}
}
}
adr = strjoker_rec(&memo, chaine, joker, size, size_flag);
if (onheap)
freet(memo.failed);
return adr;
}
/* Test-only oracle: the same matcher without the failure memo. */
const char *strjoker_nomemo(const char *chaine, const char *joker, LLint *size,
int *size_flag) {
const strjoker_memo memo = {chaine, joker, 0, NULL};
return strjoker_rec(&memo, chaine, joker, size, size_flag);
}
static const char *strjoker_impl(const strjoker_memo *memo, const char *chaine,
const char *joker, LLint *size,
int *size_flag) {
if (strnotempty(joker) == 0) { // fin de chaine joker
if (strnotempty(chaine) == 0) // fin aussi pour la chaine: ok
return chaine;
@@ -277,7 +369,7 @@ HTS_INLINE const char *strjoker(const char *chaine, const char *joker,
// tester sans le joker (pas ()+ mais ()*)
if (!unique) {
if ((adr = strjoker(chaine, joker + jmp, size, size_flag))) {
if ((adr = strjoker_rec(memo, chaine, joker + jmp, size, size_flag))) {
return adr;
}
}
@@ -289,7 +381,8 @@ HTS_INLINE const char *strjoker(const char *chaine, const char *joker,
max = strnotempty(chaine) ? 1 : 0; /* empty chaine: no char to eat */
while(i < (int) max) {
if (pass[(int) (unsigned char) chaine[i]]) { // caractère autorisé
if ((adr = strjoker(chaine + i + 1, joker + jmp, size, size_flag))) {
if ((adr = strjoker_rec(memo, chaine + i + 1, joker + jmp, size,
size_flag))) {
return adr;
}
i++;
@@ -299,7 +392,8 @@ HTS_INLINE const char *strjoker(const char *chaine, const char *joker,
// tester chaîne vide
if (i != max + 2) // avant c'est ok
if ((adr = strjoker(chaine + max, joker + jmp, size, size_flag)))
if ((adr = strjoker_rec(memo, chaine + max, joker + jmp, size,
size_flag)))
return adr;
return NULL; // perdu
@@ -321,7 +415,7 @@ HTS_INLINE const char *strjoker(const char *chaine, const char *joker,
// comparaison ok?
if (ok) {
// continuer la comparaison.
if (strjoker(chaine + jmp, joker + jmp, size, size_flag))
if (strjoker_rec(memo, chaine + jmp, joker + jmp, size, size_flag))
return chaine; // retourner 1e lettre
}

View File

@@ -41,8 +41,17 @@ Please visit our Website: http://www.httrack.com
int fa_strjoker(int type, char **filters, int nfil, const char *nom, LLint * size,
int *size_flag, int *depth);
/* fa_strjoker() on both URL forms the engine builds (nom1 full, nom2 without
scheme); the match latest in the list wins, a "don't know" verdict defers.
Returns the merged verdict; the out-params carry the winner's values. */
int fa_strjoker_dual(int type, char **filters, int nfil, const char *nom1,
const char *nom2, LLint *size, int *size_flag, int *depth);
HTS_INLINE const char *strjoker(const char *chaine, const char *joker, LLint * size,
int *size_flag);
/* strjoker() without the failure memo (exponential worst case); test-only
oracle for the memoized matcher. */
const char *strjoker_nomemo(const char *chaine, const char *joker, LLint *size,
int *size_flag);
const char *strjokerfind(const char *chaine, const char *joker);
#endif

View File

@@ -43,8 +43,8 @@ Please visit our Website: http://www.httrack.com
configure.ac, decoupled from these). VERSION is the display form, VERSIONID
the dotted numeric form, AFF_VERSION the short form shown in footers,
LIB_VERSION the data/cache format generation. */
#define HTTRACK_VERSION "3.49-11"
#define HTTRACK_VERSIONID "3.49.11"
#define HTTRACK_VERSION "3.49-12"
#define HTTRACK_VERSIONID "3.49.12"
#define HTTRACK_AFF_VERSION "3.x"
#define HTTRACK_LIB_VERSION "2.0"
@@ -56,6 +56,10 @@ Please visit our Website: http://www.httrack.com
// Platform detection (sizes, feature macros)
#include "htsconfig.h"
// Fixed-width integer types + PRI* format macros for the LLint/TStamp typedefs
#include <stdint.h>
#include <inttypes.h>
// WIN32 types
#ifdef _WIN32
#ifndef SIZEOF_LONG
@@ -111,29 +115,6 @@ Please visit our Website: http://www.httrack.com
#define HTS_DO_NOT_USE_UID
#endif
#ifndef HTS_LONGLONG
#ifdef SIZEOF_LONG_LONG
#if SIZEOF_LONG_LONG == 8
#define HTS_LONGLONG 1
#endif
#endif
#ifndef HTS_LONGLONG
#ifdef __sun
#define HTS_LONGLONG 0
#endif
#ifdef __osf__
#define HTS_LONGLONG 0
#endif
#ifdef __linux
#define HTS_LONGLONG 1
#endif
#ifdef _WIN32
#define HTS_LONGLONG 1
#endif
#endif
#endif
#ifdef DLLIB
#define HTS_DLOPEN 1
#else
@@ -326,59 +307,11 @@ typedef int hts_tristate;
#define HTS_DEPRECATED(msg)
#endif
#ifndef HTS_LONGLONG
#ifdef HTS_NO_64_BIT
#define HTS_LONGLONG 0
#else
#define HTS_LONGLONG 1
#endif
#endif
/* Wide integer types, chosen per platform.
LLint: signed 64-bit counter for byte counts and large sizes (falls back to
plain int where 64-bit is unavailable).
TStamp: timestamp/duration in the same width (a double in the no-64-bit
fallback).
LLintP: the printf conversion for an LLint. */
#if HTS_LONGLONG
#ifdef LLINT_FORMAT
typedef LLINT_TYPE LLint;
typedef LLINT_TYPE TStamp;
#define LLintP LLINT_FORMAT
#else
#ifdef _WIN32
typedef __int64 LLint;
typedef __int64 TStamp;
#define LLintP "%I64d"
#elif (defined(_LP64) || defined(__x86_64__) || defined(__powerpc64__) || \
defined(__64BIT__))
typedef long int LLint;
typedef long int TStamp;
#define LLintP "%ld"
#else
typedef long long int LLint;
typedef long long int TStamp;
#define LLintP "%lld"
#endif
#endif /* HTS_LONGLONG */
#else
typedef int LLint;
#define LLintP "%d"
typedef double TStamp;
#endif
/* LLint/TStamp: signed exact-width 64-bit; -1 is a sentinel engine-wide. */
typedef int64_t LLint;
typedef int64_t TStamp;
/* Full printf conversion, '%' included (PRId64 has none): "X: " LLintP. */
#define LLintP "%" PRId64
/* Integer type for file offsets/sizes passed to the C library. Widens to
LLint (with HTS_FSEEKO for fseeko/ftello) under large-file support, plain
@@ -422,9 +355,10 @@ typedef int T_SOC;
#endif
/* Permission bits for created folders and files (mkdir and chmod).
PROTECT_FOLDER is owner-only. With HTS_ACCESS set (the default) the ACCESS_
modes also grant group/other read; otherwise they stay owner-only. */
PROTECT_FOLDER/FILE are owner-only. With HTS_ACCESS set (the default) the
ACCESS_ modes also grant group/other read; otherwise they stay owner-only. */
#define HTS_PROTECT_FOLDER (S_IRUSR | S_IWUSR | S_IXUSR)
#define HTS_PROTECT_FILE (S_IRUSR | S_IWUSR)
#if HTS_ACCESS
#define HTS_ACCESS_FILE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)

View File

@@ -543,6 +543,7 @@ void help(const char *app, int more) {
infomsg("Spider options:");
infomsg(" bN accept cookies in cookies.txt (0=do not accept,* 1=accept)");
infomsg(" %K load extra cookies from a Netscape cookies.txt");
infomsg(" %Y explain which filter rule accepts or rejects a URL, then exit");
infomsg
(" u check document type if unknown (cgi,asp..) (u0 don't check, * u1 check but /, u2 check always)");
infomsg

View File

@@ -1562,7 +1562,7 @@ void treathead(t_cookie * cookie, const char *adr, const char *fil, htsblk * ret
}
// An empty/whitespace Content-Type value yields no token: keep the
// sentinel default rather than reading an uninitialized tempo.
if (sscanf(rcvd + p, "%s", tempo) == 1) {
if (sscanf(rcvd + p, "%1099s", tempo) == 1) { // tempo[1100], server data
if (strlen(tempo) < sizeof(retour->contenttype) - 2) // pas trop long!!
strcpybuff(retour->contenttype, tempo);
else
@@ -1657,11 +1657,11 @@ void treathead(t_cookie * cookie, const char *adr, const char *fil, htsblk * ret
if (a)
*a = '\0';
}
sscanf(a, "%s", tempo);
if (strlen(tempo) < 64) // pas trop long!!
// Bound to tempo[1100]; no token => leave empty, don't read uninit tempo.
if (sscanf(a, "%1099s", tempo) == 1 && strlen(tempo) < 64)
strcpybuff(retour->contentencoding, tempo);
else
retour->contentencoding[0] = '\0'; // erreur
retour->contentencoding[0] = '\0';
#if HTS_USEZLIB
/* Check known encodings */
if (retour->contentencoding[0]) {
@@ -3038,12 +3038,11 @@ HTSEXT_API char *int2char(strc_int2bytes2 * strc, int n) {
/* See http://physics.nist.gov/cuu/Units/binary.html */
#define ToLLint(a) ((LLint)(a))
#define ToLLintKiB (ToLLint(1024))
#define ToLLintMiB (ToLLintKiB*ToLLintKiB)
#ifdef HTS_LONGLONG
#define ToLLintGiB (ToLLintKiB*ToLLintKiB*ToLLintKiB)
#define ToLLintTiB (ToLLintKiB*ToLLintKiB*ToLLintKiB*ToLLintKiB)
#define ToLLintPiB (ToLLintKiB*ToLLintKiB*ToLLintKiB*ToLLintKiB*ToLLintKiB)
#endif
#define ToLLintMiB (ToLLintKiB * ToLLintKiB)
#define ToLLintGiB (ToLLintKiB * ToLLintKiB * ToLLintKiB)
#define ToLLintTiB (ToLLintKiB * ToLLintKiB * ToLLintKiB * ToLLintKiB)
#define ToLLintPiB \
(ToLLintKiB * ToLLintKiB * ToLLintKiB * ToLLintKiB * ToLLintKiB)
HTSEXT_API char **int2bytes2(strc_int2bytes2 * strc, LLint n) {
if (n < ToLLintKiB) {
sprintf(strc->buff1, "%d", (int) (LLint) n);
@@ -3052,9 +3051,7 @@ HTSEXT_API char **int2bytes2(strc_int2bytes2 * strc, LLint n) {
sprintf(strc->buff1, "%d,%02d", (int) ((LLint) (n / ToLLintKiB)),
(int) ((LLint) ((n % ToLLintKiB) * 100) / ToLLintKiB));
strcpybuff(strc->buff2, "KiB");
}
#ifdef HTS_LONGLONG
else if (n < ToLLintGiB) {
} else if (n < ToLLintGiB) {
sprintf(strc->buff1, "%d,%02d", (int) ((LLint) (n / (ToLLintMiB))),
(int) ((LLint) (((n % (ToLLintMiB)) * 100) / (ToLLintMiB))));
strcpybuff(strc->buff2, "MiB");
@@ -3071,13 +3068,6 @@ HTSEXT_API char **int2bytes2(strc_int2bytes2 * strc, LLint n) {
(int) ((LLint) (((n % (ToLLintPiB)) * 100) / (ToLLintPiB))));
strcpybuff(strc->buff2, "PiB");
}
#else
else {
sprintf(strc->buff1, "%d,%02d", (int) ((LLint) (n / (ToLLintMiB))),
(int) ((LLint) (((n % (ToLLintMiB)) * 100) / (ToLLintMiB))));
strcpybuff(strc->buff2, "MiB");
}
#endif
strc->buffadr[0] = strc->buff1;
strc->buffadr[1] = strc->buff2;
return strc->buffadr;
@@ -6006,6 +5996,7 @@ HTSEXT_API httrackp *hts_create_opt(void) {
StringCopy(opt->footer, HTS_DEFAULT_FOOTER);
StringCopy(opt->strip_query, "");
StringCopy(opt->cookies_file, "");
StringCopy(opt->why_url, "");
opt->pause_min_ms = 0;
opt->pause_max_ms = 0;
opt->ftp_proxy = HTS_TRUE;
@@ -6153,6 +6144,7 @@ HTSEXT_API void hts_free_opt(httrackp * opt) {
StringFree(opt->mod_blacklist);
StringFree(opt->strip_query);
StringFree(opt->cookies_file);
StringFree(opt->why_url);
StringFree(opt->path_html);
StringFree(opt->path_html_utf8);

View File

@@ -536,6 +536,8 @@ struct httrackp {
(--cookies-file) */
int pause_min_ms; /**< inter-file pause lower bound, ms (0=off, #185) */
int pause_max_ms; /**< inter-file pause upper bound, ms */
String why_url; /**< URL to diagnose (--why): print the deciding filter rule
and exit without crawling */
};
/* Running statistics for a mirror. */

View File

@@ -2007,7 +2007,7 @@ int htsparse(htsmoduleStruct * str, htsmoduleStructExtended * stre) {
/* Unescape/escape %20 and other &nbsp; */
{
// Note: always true (iso-8859-1 as default)
// NULL when UTF-8 conversion is off (-%T0)
const char *const charset = str->page_charset_;
const int hasCharset = charset != NULL
&& *charset != '\0';
@@ -2030,11 +2030,12 @@ int htsparse(htsmoduleStruct * str, htsmoduleStructExtended * stre) {
// Force to encode non-printable chars (should never happend)
escape_remove_control(lien);
// charset conversion for the URI filename,
// and not already UTF-8
// (note: not for the query string!)
if (hasCharset && !hts_isCharsetUTF8(charset)) {
// charset conversion for the URI filename (not the query
// string), unless the bytes already are valid UTF-8:
// converting those would double-encode them (#180)
if (hasCharset && !hts_isCharsetUTF8(charset) &&
!hts_isStringUTF8(lien, strlen(lien))) {
char *const s = hts_convertStringToUTF8(lien, strlen(lien), charset);
if (s != NULL) {
hts_log_print(opt, LOG_DEBUG,

View File

@@ -46,6 +46,7 @@ Please visit our Website: http://www.httrack.com
#include "htsdefines.h"
#include "htslib.h"
#include "htsparse.h"
#include "htscache.h"
#include "htscache_selftest.h"
#include "htsdns_selftest.h"
#include "htscharset.h"
@@ -620,6 +621,125 @@ static int st_filtersize(httrackp *opt, int argc, char **argv) {
return 0;
}
/* Mime-type filter verdict via fa_strjoker(type=1): only mime: rules apply. */
static int st_filtermime(httrackp *opt, int argc, char **argv) {
int verdict;
(void) opt;
if (argc < 2) {
fprintf(stderr, "filtermime: needs <mime> <filter> [filter...]\n");
return 1;
}
verdict = fa_strjoker(1, &argv[1], argc - 1, argv[0], NULL, NULL, NULL);
printf("verdict=%s\n", verdict > 0 ? "allowed"
: verdict < 0 ? "forbidden"
: "unknown");
return 0;
}
/* SplitMix64: deterministic, platform-independent case generator. */
static uint64_t st_mix64(uint64_t *state) {
uint64_t z = (*state += UINT64_C(0x9E3779B97F4A7C15));
z = (z ^ (z >> 30)) * UINT64_C(0xBF58476D1CE4B5B9);
z = (z ^ (z >> 27)) * UINT64_C(0x94D049BB133111EB);
return z ^ (z >> 31);
}
/* Differential test: memoized strjoker vs the no-memo oracle on seeded random
pattern/subject/size cases must agree on result, *size and *size_flag. */
static int st_filtermemo(httrackp *opt, int argc, char **argv) {
static const char *const pieces[] = {
"a", "b", "A", "c", ".", "/",
"?", "*", "*[a]", "*[ab]", "*[a-c]", "*[A-Z]",
"*[<5]", "*[>5]", "*(a)", "*(a,b)", "*[file]", "*[path]",
"*[name]", "*[param]", "*[]", "*[\\a]", "*[a-"};
static const char subject_chars[] = "abAc./?";
uint64_t rng = UINT64_C(0x501);
int iters = 20000, matched = 0, unmatched = 0;
int it;
(void) opt;
if (argc >= 1)
sscanf(argv[0], "%d", &iters);
for (it = 0; it < iters; it++) {
char pat[64], str[16];
size_t pl = 0;
const int npieces = (int) (st_mix64(&rng) % 5);
const int slen = (int) (st_mix64(&rng) % 13);
const int szsel = (int) (st_mix64(&rng) % 4); /* none/unknown/3/20 */
LLint sz1, sz2, off1, off2;
int i, flag1 = 0, flag2 = 0;
char *hpat, *hstr;
const char *adr;
for (i = 0; i < npieces; i++) {
const char *p =
pieces[st_mix64(&rng) % (sizeof(pieces) / sizeof(pieces[0]))];
const size_t len = strlen(p);
if (len >= sizeof(pat) - pl)
break;
memcpy(pat + pl, p, len);
pl += len;
}
pat[pl] = '\0';
for (i = 0; i < slen; i++)
str[i] = subject_chars[st_mix64(&rng) % (sizeof(subject_chars) - 1)];
str[slen] = '\0';
sz1 = sz2 = (szsel <= 1) ? -1 : (szsel == 2) ? 3 : 20;
/* exact-size heap copies per run so a sanitizer traps any over-read */
hpat = strdupt(pat);
hstr = strdupt(str);
adr = strjoker(hstr, hpat, szsel ? &sz1 : NULL, szsel ? &flag1 : NULL);
off1 = adr != NULL ? (LLint) (adr - hstr) : -1;
freet(hpat);
freet(hstr);
hpat = strdupt(pat);
hstr = strdupt(str);
adr =
strjoker_nomemo(hstr, hpat, szsel ? &sz2 : NULL, szsel ? &flag2 : NULL);
off2 = adr != NULL ? (LLint) (adr - hstr) : -1;
freet(hpat);
freet(hstr);
if (off1 != off2 || sz1 != sz2 || flag1 != flag2) {
printf("filtermemo MISMATCH pat=[%s] str=[%s] szsel=%d: "
"off %d/%d size %d/%d flag %d/%d\n",
pat, str, szsel, (int) off1, (int) off2, (int) sz1, (int) sz2,
flag1, flag2);
return 1;
}
if (off1 >= 0)
matched++;
else
unmatched++;
}
/* both polarities must actually occur or the test proves nothing */
assertf(matched > 0 && unmatched > 0);
printf("filtermemo: %d cases OK\n", iters);
return 0;
}
/* Merged two-form filter verdict via fa_strjoker_dual (see htsfilters.h). */
static int st_filterdual(httrackp *opt, int argc, char **argv) {
int depth = -1, verdict;
(void) opt;
if (argc < 3) {
fprintf(stderr,
"filterdual: needs <string1> <string2> <filter> [filter...]\n");
return 1;
}
verdict = fa_strjoker_dual(0, &argv[2], argc - 2, argv[0], argv[1], NULL,
NULL, &depth);
printf("verdict=%s rule=%d\n",
verdict > 0 ? "allowed"
: verdict < 0 ? "forbidden"
: "unknown",
depth);
return 0;
}
static int st_simplify(httrackp *opt, int argc, char **argv) {
(void) opt;
if (argc < 1) {
@@ -668,6 +788,30 @@ static int st_charset(httrackp *opt, int argc, char **argv) {
return 0;
}
static int st_metacharset(httrackp *opt, int argc, char **argv) {
char *s;
(void) opt;
if (argc < 1) {
fprintf(stderr, "metacharset: needs an html string\n");
return 1;
}
s = hts_getCharsetFromMeta(argv[0], strlen(argv[0]));
printf("%s\n", s != NULL ? s : "(none)");
freet(s);
return 0;
}
static int st_isutf8(httrackp *opt, int argc, char **argv) {
(void) opt;
if (argc < 1) {
fprintf(stderr, "isutf8: needs a string\n");
return 1;
}
printf("%d\n", hts_isStringUTF8(argv[0], strlen(argv[0])) ? 1 : 0);
return 0;
}
static int st_idna_encode(httrackp *opt, int argc, char **argv) {
char *s;
@@ -1143,6 +1287,26 @@ static int st_header(httrackp *opt, int argc, char **argv) {
treathead(NULL, "www.example.com", "/", &r, line);
}
printf("contenttype=%s cdispo=%s\n", r.contenttype, r.cdispo);
printf("contentencoding=%s\n", r.contentencoding);
return 0;
}
/* An over-long header value must not overflow treathead's tempo[1100]. */
static int st_headerlong(httrackp *opt, int argc, char **argv) {
htsblk r;
char BIGSTK line[HTS_URLMAXSIZE * 2];
const char *const name = argc >= 1 ? argv[0] : "Content-Type:";
const int pad = 1500; /* > tempo[1100] */
size_t n;
(void) opt;
memset(&r, 0, sizeof(r));
n = (size_t) snprintf(line, sizeof(line), "%s ", name);
memset(line + n, 'a', pad);
line[n + pad] = '\0';
treathead(NULL, "www.example.com", "/", &r, line);
printf("contenttype_len=%d contentencoding_len=%d\n",
(int) strlen(r.contenttype), (int) strlen(r.contentencoding));
return 0;
}
@@ -1350,6 +1514,78 @@ static int st_cache(httrackp *opt, int argc, char **argv) {
return err;
}
/* A corrupt cache index (.ndx) must not walk the length-prefixed scan past
the buffer. Checks the two primitives the loader is built from. */
static int st_cacheindex(httrackp *opt, int argc, char **argv) {
int fail = 0;
(void) opt;
(void) argc;
(void) argv;
/* A length prefix that overstates the bytes present must bound the advance
to the buffer, not trust the declared length. */
{
static const char src[] = "32768\nCACHE-1.1";
const size_t len = sizeof(src) - 1;
char *buf = malloct(len + 1);
char s[256];
int off;
memcpy(buf, src, len + 1);
off = cache_brstr(buf, s, sizeof(s));
if (off > (int) len) {
printf("cacheindex: over-advance off=%d len=%d\n", off, (int) len);
fail = 1;
}
if (strcmp(s, "CACHE-1.1") != 0) {
printf("cacheindex: value=%s\n", s);
fail = 1;
}
freet(buf);
}
/* cache_binput reads a field while in bounds, but refuses one starting at
or past end-of-buffer. */
{
char buf[8] = "ab\ncd";
const char *const end = buf + 5;
char s[16];
if (cache_binput(buf, end, s, sizeof(s)) != 3 || strcmp(s, "ab") != 0)
fail = 1; /* normal read: "ab" then the '\n', 3 bytes consumed */
if (cache_binput(end, end, s, sizeof(s)) != 0 || s[0] != '\0')
fail = 1;
}
/* Drive the full loader scan over a truncated index: a declared length that
overshoots plus a half-written entry. ASan aborts here on the pre-fix
scan; the cursor must never leave the buffer. */
{
static const char src[] = "9\nCACHE-1.1\n99\nwww.example.com\n/a";
const size_t len = sizeof(src) - 1;
char *buf = malloct(len + 1);
const char *const end = buf + len;
char line[256];
char *a = buf;
memcpy(buf, src, len + 1);
a += cache_brstr(a, line, sizeof(line));
a += cache_brstr(a, line, sizeof(line));
while (a != NULL && a < end) {
a = strchr(a + 1, '\n');
if (a == NULL)
break;
a++;
a += cache_binput(a, end, line, sizeof(line));
}
freet(buf);
}
printf("cacheindex: %s\n", fail ? "FAIL" : "OK");
return fail;
}
static int st_cache_golden(httrackp *opt, int argc, char **argv) {
int regen, err;
@@ -1387,6 +1623,18 @@ static int st_cache_corrupt(httrackp *opt, int argc, char **argv) {
return err;
}
static int st_cache_legacy(httrackp *opt, int argc, char **argv) {
int err;
if (argc < 1) {
fprintf(stderr, "cache-legacy: needs a directory\n");
return 1;
}
err = cache_legacy_refused_selftest(opt, argv[0]);
printf("cache-legacy: %s\n", err ? "FAIL" : "OK");
return err;
}
static int st_reconcile(httrackp *opt, int argc, char **argv) {
int err;
@@ -1438,6 +1686,26 @@ static int st_cookies(httrackp *opt, int argc, char **argv) {
err = 1;
if (strstr(hdr, "junk") != NULL) // wrong-domain cookie leaked
err = 1;
#ifndef _WIN32
/* the jar holds live session cookies: cookie_save must keep it 0600 */
{
const char *jar = "st-cookies-jar.txt";
struct stat st;
(void) UNLINK(jar);
assertf(cookie_save(&cookie, jar) == 0);
assertf(stat(jar, &st) == 0);
assertf((st.st_mode & 07777) == HTS_PROTECT_FILE);
assertf(st.st_size > 0); /* mode-only checks would pass an empty jar */
/* a pre-existing world-readable jar must be tightened, not kept */
assertf(chmod(jar, 0644) == 0);
assertf(cookie_save(&cookie, jar) == 0);
assertf(stat(jar, &st) == 0);
assertf((st.st_mode & 07777) == HTS_PROTECT_FILE);
assertf(st.st_size > 0);
(void) UNLINK(jar);
}
#endif
printf("cookie-header: %s\n", err ? "FAIL" : "OK");
if (err)
printf(" got: %s\n", hdr);
@@ -1879,6 +2147,19 @@ static int ae_write_collision(const char *path, const unsigned char *src,
return ok ? 0 : 1;
}
/* Write src[0..len) to path as-is; 0 on success. */
static int ae_write_raw(const char *path, const unsigned char *src,
size_t len) {
FILE *const f = FOPEN(path, "wb");
int ok;
if (f == NULL)
return 1;
ok = fwrite(src, 1, len, f) == len;
fclose(f);
return ok ? 0 : 1;
}
/* Compare path's bytes to expect[0..len); 0 if equal. Streams (large files). */
static int ae_check_decoded(const char *path, const unsigned char *expect,
size_t len) {
@@ -1946,6 +2227,46 @@ static int st_acceptencoding(httrackp *opt, int argc, char **argv) {
assertf(ae_write_collision(inpath, body, 64) == 0);
assertf(hts_zunpack(inpath, outpath) == 64);
assertf(ae_check_decoded(outpath, body, 64) == 0);
/* Identity fallback (#47): a plain body mislabeled as compressed is kept
verbatim, small and multi-chunk (> one 8 KiB fread). */
assertf(ae_write_raw(inpath, small, slen) == 0);
assertf(hts_zunpack(inpath, outpath) == (int) slen);
assertf(ae_check_decoded(outpath, small, slen) == 0);
{
const size_t ilen = 16 * 1024;
unsigned char *idbody = malloct(ilen);
assertf(idbody != NULL);
for (i = 0; i < ilen; i++)
idbody[i] = small[i % slen];
assertf(ae_write_raw(inpath, idbody, ilen) == 0);
assertf(hts_zunpack(inpath, outpath) == (int) ilen);
assertf(ae_check_decoded(outpath, idbody, ilen) == 0);
freet(idbody);
}
/* Truncated gzip (CRC+ISIZE cut), zlib (ADLER32 cut) and raw deflate
must all still fail, not fall back to a verbatim copy. */
{
static const struct {
int wb;
size_t cut;
} tr[] = {{16 + MAX_WBITS, 8}, {MAX_WBITS, 4}, {-MAX_WBITS, 5}};
for (i = 0; i < sizeof(tr) / sizeof(tr[0]); i++) {
unsigned char z[512];
size_t zlen;
FILE *f;
assertf(ae_write_packed(inpath, tr[i].wb, small, slen) == 0);
f = FOPEN(inpath, "rb");
assertf(f != NULL);
zlen = fread(z, 1, sizeof(z), f);
fclose(f);
assertf(zlen > tr[i].cut && zlen < sizeof(z));
assertf(ae_write_raw(inpath, z, zlen - tr[i].cut) == 0);
assertf(hts_zunpack(inpath, outpath) < 0);
}
}
freet(body);
}
#else
@@ -2134,6 +2455,12 @@ static const struct selftest_entry {
{"filtersize", "<size> <string> <filter>...",
"size-aware filter verdict (negative size = unknown/scan time)",
st_filtersize},
{"filtermime", "<mime> <filter>...",
"mime-type filter verdict (fa_strjoker type=1)", st_filtermime},
{"filtermemo", "[iterations]",
"memoized vs unmemoized matcher differential", st_filtermemo},
{"filterdual", "<string1> <string2> <filter>...",
"merged two-form filter verdict (fa_strjoker_dual)", st_filterdual},
{"simplify", "<path>", "collapse ./ and ../ in a path", st_simplify},
{"stripquery", "", "--strip-query pattern/key stripping self-test",
st_stripquery},
@@ -2144,6 +2471,9 @@ static const struct selftest_entry {
{"mime", "<filename>", "MIME type for a filename", st_mime},
{"charset", "<charset> <string>",
"convert a string to UTF-8 from a charset", st_charset},
{"metacharset", "<html>", "extract the <meta> charset from an HTML page",
st_metacharset},
{"isutf8", "<string>", "is the string valid UTF-8 (1/0)", st_isutf8},
{"idna-encode", "<host>", "encode a hostname to IDNA/punycode",
st_idna_encode},
{"idna-decode", "<host>", "decode an IDNA/punycode hostname",
@@ -2163,17 +2493,24 @@ static const struct selftest_entry {
{"identurl", "<url>", "split an absolute URL into (adr, fil)", st_identurl},
{"header", "<raw-header-line> ...", "response header-line parsing",
st_header},
{"headerlong", "[header-name:]",
"over-long header value must not overflow the parse scratch",
st_headerlong},
{"savename", "<fil> <content-type> [key=value ...]",
"local save-name for a URL", st_savename},
{"sniff", "<content-type> <hex:..|text>", "MIME magic consistency",
st_sniff},
{"cache", "<dir>", "cache read/write round-trip self-test", st_cache},
{"cacheindex", "", "cache-index (.ndx) parse must stay in bounds",
st_cacheindex},
{"cache-golden", "<dir> [regen]", "frozen cache-format read self-test",
st_cache_golden},
{"cache-writefail", "<dir>", "cache write-failure handling self-test",
st_cache_writefail},
{"reconcile", "<dir>", "cache generation reconcile policy self-test",
st_reconcile},
{"cache-legacy", "<dir>", "pre-3.31 legacy cache refusal self-test",
st_cache_legacy},
{"cache-corrupt", "<dir>", "cache read-side corruption self-test",
st_cache_corrupt},
{"dns", "", "DNS resolver/cache self-test", st_dns},

View File

@@ -524,28 +524,11 @@ static int hts_acceptlink_(httrackp * opt, int ptr,
// filters, 0=sait pas 1=ok -1=interdit
{
int jokDepth1 = 0, jokDepth2 = 0;
int jok1 = 0, jok2 = 0;
int jokDepth = 0;
jok1 =
fa_strjoker( /*url */ 0, _FILTERS, *_FILTERS_PTR, lfull, NULL, NULL,
&jokDepth1);
jok2 =
fa_strjoker( /*url */ 0, _FILTERS, *_FILTERS_PTR, l, NULL, NULL,
&jokDepth2);
if (jok2 == 0) { // #2 doesn't know
jok = jok1; // then, use #1
mdepth = _FILTERS[jokDepth1];
} else if (jok1 == 0) { // #1 doesn't know
jok = jok2; // then, use #2
mdepth = _FILTERS[jokDepth2];
} else if (jokDepth1 >= jokDepth2) { // #1 matching rule is "after" #2, then it is prioritary
jok = jok1;
mdepth = _FILTERS[jokDepth1];
} else { // #2 matching rule is "after" #1, then it is prioritary
jok = jok2;
mdepth = _FILTERS[jokDepth2];
}
jok = fa_strjoker_dual(/*url */ 0, _FILTERS, *_FILTERS_PTR, lfull, l,
NULL, NULL, &jokDepth);
mdepth = _FILTERS[jokDepth];
}
if (jok == 1) { // autorisé
@@ -965,34 +948,10 @@ int hts_testlinksize(httrackp * opt, const char *adr, const char *fil, LLint siz
// filters, 0=sait pas 1=ok -1=interdit
{
int jokDepth1 = 0, jokDepth2 = 0;
int jok1 = 0, jok2 = 0;
LLint sz1 = size, sz2 = size;
int size_flag1 = 0, size_flag2 = 0;
jok1 =
fa_strjoker( /*url */ 0, *opt->filters.filters, *opt->filters.filptr,
lfull, &sz1, &size_flag1, &jokDepth1);
jok2 =
fa_strjoker( /*url */ 0, *opt->filters.filters, *opt->filters.filptr,
l, &sz2, &size_flag2, &jokDepth2);
if (jok2 == 0) { // #2 doesn't know
jok = jok1; // then, use #1
sz = sz1;
size_flag = size_flag1;
} else if (jok1 == 0) { // #1 doesn't know
jok = jok2; // then, use #2
sz = sz2;
size_flag = size_flag2;
} else if (jokDepth1 >= jokDepth2) { // #1 matching rule is "after" #2, then it is prioritary
jok = jok1;
sz = sz1;
size_flag = size_flag1;
} else { // #2 matching rule is "after" #1, then it is prioritary
jok = jok2;
sz = sz2;
size_flag = size_flag2;
}
sz = size;
jok = fa_strjoker_dual(/*url */ 0, *opt->filters.filters,
*opt->filters.filptr, lfull, l, &sz, &size_flag,
NULL);
}
// log

View File

@@ -48,6 +48,7 @@ Please visit our Website: http://www.httrack.com
/*
Unpack file into a new file (gzip, zlib RFC1950 or raw deflate RFC1951).
A body provably in no deflate framing is copied verbatim (identity).
Return value: size of the new file, or -1 if an error occurred
*/
/* Note: utf-8 */
@@ -68,6 +69,10 @@ int hts_zunpack(char *filename, char *newfile) {
((inbuf[0] & 0x0f) == Z_DEFLATED &&
(((unsigned) inbuf[0] << 8 | inbuf[1]) % 31) == 0)));
int attempt;
/* not_deflate: the raw attempt hit a data error, so the body is in no
deflate framing at all. env_error: local I/O or memory failure. */
hts_boolean not_deflate = HTS_FALSE;
hts_boolean env_error = HTS_FALSE;
/* deflate is ambiguous; on failure retry with the other windowBits */
for (attempt = 0; attempt < 2 && ret < 0; attempt++) {
@@ -78,15 +83,20 @@ int hts_zunpack(char *filename, char *newfile) {
if (attempt > 0) {
/* rewind input; reopening fpout "wb" discards the partial output */
if (fseek(in, 0, SEEK_SET) != 0)
if (fseek(in, 0, SEEK_SET) != 0) {
env_error = HTS_TRUE;
break;
}
navail = fread(inbuf, 1, sizeof(inbuf), in);
}
fpout = FOPEN(fconv(catbuff, sizeof(catbuff), newfile), "wb");
if (fpout == NULL)
if (fpout == NULL) {
env_error = HTS_TRUE;
break;
}
memset(&strm, 0, sizeof(strm));
if (inflateInit2(&strm, windowBits) != Z_OK) {
env_error = HTS_TRUE;
fclose(fpout);
break;
}
@@ -108,12 +118,17 @@ int hts_zunpack(char *filename, char *newfile) {
zerr = inflate(&strm, Z_NO_FLUSH);
if (zerr == Z_NEED_DICT || zerr == Z_DATA_ERROR ||
zerr == Z_MEM_ERROR || zerr == Z_STREAM_ERROR) {
if (zerr == Z_MEM_ERROR || zerr == Z_STREAM_ERROR)
env_error = HTS_TRUE;
else if (windowBits < 0)
not_deflate = HTS_TRUE;
ok = HTS_FALSE;
break;
}
produced = sizeof(outbuf) - strm.avail_out;
if (produced > 0 &&
fwrite(outbuf, 1, produced, fpout) != produced) {
env_error = HTS_TRUE;
ok = HTS_FALSE;
break;
}
@@ -129,6 +144,28 @@ int hts_zunpack(char *filename, char *newfile) {
inflateEnd(&strm);
fclose(fpout);
}
/* keep a mislabeled identity body verbatim only when provably not
deflate; truncation or a local failure must keep failing (#47) */
if (ret < 0 && !wrapped && not_deflate && !env_error && !ferror(in)) {
FILE *const fpout =
FOPEN(fconv(catbuff, sizeof(catbuff), newfile), "wb");
if (fpout != NULL && fseek(in, 0, SEEK_SET) == 0) {
int size = 0;
while ((navail = fread(inbuf, 1, sizeof(inbuf), in)) > 0) {
if (fwrite(inbuf, 1, navail, fpout) != navail) {
size = -1;
break;
}
size += (int) navail;
}
if (size >= 0 && !ferror(in))
ret = size;
}
if (fpout != NULL)
fclose(fpout);
}
fclose(in);
}
}

10
tests/01_engine-cacheindex.test Executable file
View File

@@ -0,0 +1,10 @@
#!/bin/bash
#
set -euo pipefail
# Cache-index (.ndx) parse must stay inside the buffer on a corrupt length
# prefix (ASan aborts here on the pre-fix binary; the OK check gates the
# non-sanitized build too).
test "$(httrack -O /dev/null -#test=cacheindex)" == 'cacheindex: OK'

View File

@@ -36,3 +36,64 @@ test "$(httrack -O /dev/null -#test=charset 'no-such-charset-xyz' 'café' 2>/dev
# malformed UTF-8 (lone continuation byte, truncated lead byte) must not crash
runs 'utf-8' $'\x80'
runs 'utf-8' $'\xc3'
# hts_getCharsetFromMeta: <meta> charset extraction, HTML5 and legacy forms.
# -#test=metacharset <html> prints the charset, or "(none)".
meta() {
test "$(httrack -O /dev/null -#test=metacharset "$1")" == "$2" || exit 1
}
# HTML5 form, quoting/spacing flavors
meta '<meta charset="utf-8">' 'utf-8'
meta "<meta charset='utf-8'>" 'utf-8'
meta '<meta charset=utf-8>' 'utf-8'
meta '<meta charset=utf-8 />' 'utf-8'
meta '<meta charset=utf-8/>' 'utf-8'
# charset labels are case-insensitive downstream; only require case-insensitive match
test "$(httrack -O /dev/null -#test=metacharset '<META CHARSET=UTF-8>' | tr '[:upper:]' '[:lower:]')" == 'utf-8' || exit 1
meta '<html><head><meta charset = "utf-8" ></head>' 'utf-8'
# legacy http-equiv form, attributes in any order
meta '<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">' 'iso-8859-1'
meta '<meta content="text/html; charset=gb2312" http-equiv="content-type">' 'gb2312'
meta '<meta http-equiv=Content-Type content="text/html;charset=euc-jp">' 'euc-jp'
meta '<meta http-equiv="Content-Type" content="text/html; charset = koi8-r ">' 'koi8-r'
meta '<meta http-equiv="Content-Type" content="text/html; charset=utf-8;">' 'utf-8'
meta '<meta http-equiv=content-type content=text/html;charset=utf-8 />' 'utf-8'
meta '<meta http-equiv=content-type content=text/html;charset=utf-8>' 'utf-8'
# first resolvable meta wins; unrelated metas are skipped
meta '<meta name="viewport" content="width=1"><meta charset="koi8-r">' 'koi8-r'
meta '<meta charset="utf-8"><meta http-equiv="Content-Type" content="text/html; charset=gb2312">' 'utf-8'
# duplicate attribute: first wins, as in browsers
meta '<meta http-equiv="Content-Type" content="text/html; charset=big5" http-equiv="refresh">' 'big5'
# negatives: wrong tag/attribute, empty value
meta '<p>x</p>' '(none)'
meta '<meta name="desc" content="charset=bogus">' '(none)'
meta '<meta http-equiv="refresh" content="0; url=x.html">' '(none)'
meta '<body charset="utf-8">' '(none)'
meta '<meta charset="">' '(none)'
# hostile truncations: no crash, no match
meta '<meta charset="utf-8' '(none)'
meta '<meta charset=' '(none)'
meta '<meta ' '(none)'
meta '<meta' '(none)'
# hts_isStringUTF8: valid-UTF-8 probe (guards the #180 link conversion).
utf8() {
test "$(httrack -O /dev/null -#test=isutf8 "$1")" == "$2" || exit 1
}
utf8 'plain' '1'
utf8 'café' '1'
utf8 '统计' '1'
utf8 "$(printf 'caf\xe9')" '0' # latin-1 accent
utf8 "$(printf '\xc3')" '0' # truncated lead byte
utf8 "$(printf '\xa9')" '0' # lone continuation byte
utf8 "$(printf '\xf0\x9f\x98\x80')" '1' # 4-byte emoji
utf8 "$(printf '\xc0\xaf')" '0' # overlong (latin-1 "À¯" must stay convertible)
utf8 "$(printf '\xed\xa0\x80')" '0' # UTF-16 surrogate
utf8 "$(printf '\xf4\x90\x80\x80')" '0' # above U+10FFFF
utf8 "$(printf '\xf8\x88\x80\x80\x80')" '0' # legacy 5-byte form

View File

@@ -131,3 +131,64 @@ match '*.aspx' 'page.aspx?y=2'
# empty subject against a unique-match allow-all pattern (heap over-read regress)
nomatch_empty '*(('
nomatch_empty '**(('
# near-miss negatives: word order, exact tails, range boundaries, class case
nomatch 'foo*bar' 'foobaz'
nomatch '*foo*bar' 'barfoo' # both words present, wrong order
nomatch '*.gif' 'a.gifx' # trailing junk after the extension
nomatch '*.gif' 'a.gi'
nomatch '*[b-y]' 'a'
nomatch '*[b-y]' 'z'
nomatch '*[a-z].txt' 'B.txt' # classes are case-sensitive...
match 'FoO*' 'foobar' # ...literals are not
# a star may match an empty run, but never skip a required literal
match '*[0-9].gif' '.gif'
nomatch '*[0-9].gif' 'x.gif'
# a descending range *[c-a] is an empty class, not c..a reversed
match '*[c-a]x' 'x'
nomatch '*[c-a]x' 'cx'
# *[param] is all-or-nothing on '?': spans everything after one, else nothing
match 'a*[param]' 'a?x=1'
nomatch 'a*[param]' 'ab?x=1'
# the latest matching rule wins, regardless of sign
fsize 'verdict=forbidden size_flag=0' -1 a.gif '+*.gif' '-*.gif'
fsize 'verdict=allowed size_flag=0' -1 a.gif '-*.gif' '+*.gif'
# a failed size test sets size_flag but leaves the rule unmatched
fsize 'verdict=unknown size_flag=1' 5 foo.jpg '-*.jpg*[>10]'
# mime filters (-#test=filtermime = fa_strjoker type=1): only mime: rules
# apply to a mime type, and mime: rules never apply to a URL
fmime() {
local want="$1"
shift
test "$(httrack -O /dev/null -#test=filtermime "$@")" == "verdict=$want" || exit 1
}
fmime allowed 'image/png' '+mime:image/*'
fmime unknown 'text/html' '+mime:image/*'
fmime forbidden 'image/png' '+mime:image/*' '-mime:image/png' # last wins
fmime allowed 'image/png' '-mime:image/png' '+mime:image/*'
fmime unknown 'image/png' '+image/*' # URL rule: skipped
fsize 'verdict=unknown size_flag=0' -1 'image/png' '+mime:image/*' # and vice versa
# memoized vs no-memo differential over seeded random patterns and subjects,
# matching and non-matching (the self-test asserts both polarities occurred)
test "$(httrack -O /dev/null -#test=filtermemo)" == "filtermemo: 20000 cases OK" || exit 1
# star-heavy non-match must stay polynomial (#501); 400 chars also exercises
# the heap-allocated memo. timeout (where available) turns a hang into a FAIL.
with_timeout() {
if command -v timeout >/dev/null; then timeout 60 "$@"; else "$@"; fi
}
pat=$(printf '*[a]%.0s' $(seq 1 12))b
subj=$(printf 'a%.0s' $(seq 1 400))
test "$(with_timeout httrack -O /dev/null -#test=filter "$pat" "$subj")" == "$subj does NOT match $pat" || exit 1
# a later star's class dead-end must re-extend the earlier star, not give up
# (guards against a single-backtrack-point matcher rewrite)
match '*[aX]X*[a]Y' 'aXaXaY'
nomatch '*[aX]X*[a]Y' 'aXbXaY'

View File

@@ -0,0 +1,20 @@
#!/bin/bash
#
set -euo pipefail
# fa_strjoker_dual: merged verdict on the two URL forms the engine tests;
# the match latest in the filter list wins, an unknown verdict defers.
fdual() {
local want="$1"
shift
test "$(httrack -O /dev/null -#test=filterdual "$@")" == "$want" || exit 1
}
fdual 'verdict=unknown rule=0' zzz yyy '+a*' # neither form matches
fdual 'verdict=forbidden rule=0' aaa zzz '-a*' # only form 1 matches
fdual 'verdict=forbidden rule=0' zzz aaa '-a*' # only form 2 matches
fdual 'verdict=forbidden rule=1' a b '+a*' '-b*' # later rule wins (form 2)
fdual 'verdict=forbidden rule=1' b a '+a*' '-b*' # later rule wins (form 1)
fdual 'verdict=allowed rule=0' a aa '+a*' # tie: form 1 wins

View File

@@ -27,3 +27,16 @@ hdr 'contenttype= cdispo=report.pdf' \
# Path components in the filename are dropped on the wire (RFC 2616).
hdr 'contenttype= cdispo=evil.pdf' \
'Content-Disposition: attachment; filename="../../evil.pdf"'
# An over-long header value must not overflow the parse scratch (ASan aborts
# here on the pre-fix binary).
test "$(httrack -O /dev/null -#test=headerlong 'Content-Type:')" \
== 'contenttype_len=32 contentencoding_len=0'
test "$(httrack -O /dev/null -#test=headerlong 'Content-Encoding:')" \
== 'contenttype_len=0 contentencoding_len=0'
# An empty Content-Encoding yields no token: leave it empty, don't read the
# uninitialized scratch (MSan aborts here on the pre-fix binary).
enc() { httrack -O /dev/null -#test=header "$1" | grep '^contentencoding='; }
test "$(enc 'Content-Encoding:')" == 'contentencoding='
test "$(enc 'Content-Encoding: gzip')" == 'contentencoding=gzip'

View File

@@ -0,0 +1,23 @@
#!/bin/bash
#
# The pre-3.31 .dat/.ndx cache import was removed: cache_init must refuse the
# legacy pair and leave the files untouched (httrack -#test=cache-legacy <dir>).
set -eu
dir=$(mktemp -d)
trap '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)
cnt=$(printf '%s\n' "$out" | grep -c 'no longer supported' || true)
test "$cnt" = 2 || {
echo "expected 2 refusal messages, got $cnt" >&2
exit 1
}
out=$(printf '%s\n' "$out" | tail -n1)
test "$out" = "cache-legacy: OK" || {
echo "expected 'cache-legacy: OK', got: $out" >&2
exit 1
}

View File

@@ -22,9 +22,9 @@ bash crawl-test.sh \
httrack http://ut.httrack.com/unicode-links/utf8.html
bash crawl-test.sh \
--errors 4 --files 7 \
--found ut.httrack.com/unicode-links/café3860.html \
--found ut.httrack.com/unicode-links/café9fa8.html \
--errors 2 --files 9 \
--found ut.httrack.com/unicode-links/café3860.html \
--found ut.httrack.com/unicode-links/café9fa8.html \
--found ut.httrack.com/unicode-links/café30f4.html \
--found ut.httrack.com/unicode-links/café5e1f.html \
--found ut.httrack.com/unicode-links/café7b30.html \

View File

@@ -66,9 +66,12 @@ test -n "$port" || {
}
out="$tmpdir/crawl"
# macOS has only 127.0.0.1, so the dead 127.0.0.x connects block to the timeout
# instead of refusing instantly (as on Linux); a small --timeout + --retries=0
# bound the stall without changing what the fallback exercises.
HTTRACK_DEBUG_RESOLVE="deadhost:127.0.0.2,127.0.0.1" \
httrack "http://deadhost:$port/simple/basic.html" -O "$out" \
-c1 --robots=0 --timeout=30 --quiet -Z >"$tmpdir/log" 2>&1
-c1 --robots=0 --timeout=5 --retries=0 --quiet -Z >"$tmpdir/log" 2>&1
log="$out/hts-log.txt"
@@ -92,12 +95,12 @@ test -f "$out/deadhost_$port/simple/basic.html" || {
exit 1
}
# every address refused: the slot exhausts the list, then errors out (the
# harness timeout would catch a hang/loop; refused connects are instant)
# every address dead: the slot exhausts the list, then errors out (the harness
# timeout would catch a hang/loop)
out2="$tmpdir/crawl2"
HTTRACK_DEBUG_RESOLVE="alldead:127.0.0.2,127.0.0.3" \
httrack "http://alldead:$port/simple/basic.html" -O "$out2" \
-c1 --robots=0 --timeout=30 --quiet -Z >"$tmpdir/log2" 2>&1
-c1 --robots=0 --timeout=5 --retries=0 --quiet -Z >"$tmpdir/log2" 2>&1
log2="$out2/hts-log.txt"
grep -q "trying next address" "$log2" || {

View File

@@ -17,7 +17,10 @@ printf 'GIF89a' >"$tmproot/javaclass/hello.gif"
printf '\xCA\xFE\xBA\xBE\x00\x00\x00\x32\x00\x02\x01\x00\x09hello.gif\x00\x00\x00\x00' \
>"$tmproot/javaclass/Foo.class"
# --log-found first: a plugin-load failure (wrong soname, shadowing system
# libhtsjava) trips here on the banner suffix, not as an opaque "not found".
bash "$top_srcdir/tests/local-crawl.sh" --root "$tmproot" --errors 0 \
--log-found '\+libhtsjava' \
--found 'javaclass/Foo.class' \
--found 'javaclass/hello.gif' \
httrack 'BASEURL/javaclass/index.html'

View File

@@ -1,7 +1,7 @@
#!/bin/bash
#
# Diverse seeded /big/ crawl: 12 pattern families, decoy absence, update pass
# must 304-revalidate. 360 = 1 index + 96 pages + 192 imgs + 5 shared + 60
# must 304-revalidate. 361 = 1 index + 96 pages + 192 imgs + 5 shared + 61
# family + 6 singles; the 4 planted errors write -o1 pages, not counted.
set -euo pipefail
@@ -9,7 +9,7 @@ set -euo pipefail
: "${top_srcdir:=..}"
bash "$top_srcdir/tests/local-crawl.sh" --rerun \
--errors 4 --files 360 \
--errors-content 4 --files 361 \
--found 'big/p/95.html' \
--found 'big/a/d1/d2/d3/d4/d5/d6/d7/d8/deep.png' \
--found 'big/a/f2-2x.png' \
@@ -40,6 +40,7 @@ bash "$top_srcdir/tests/local-crawl.sh" --rerun \
--not-found 'big/x/bj.jar' \
--not-found 'big/x/is1.png' \
--not-found 'big/x/concat.html' \
--file-matches 'big/f1/gzid.html' '<p>gzid</p>' \
--file-matches 'big/p/2.html' 'srcset="\.\./a/f2-1x\.png 1x, \.\./a/f2-2x\.png 2x"' \
--file-matches 'big/a/blk2.css' 'url\(blk2-bg\.png\)' \
--file-matches 'big/p/5.html' "document\\.write\\('<a href=\"\\.\\./f5/dw\\.html\"" \

35
tests/40_local-why.test Normal file
View File

@@ -0,0 +1,35 @@
#!/bin/bash
#
set -euo pipefail
# --why: report which +/- filter rule decides for a URL, without crawling.
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
why() {
local want="$1" out
shift
out="$(httrack -O "$tmpdir/p" -q "$@" |
grep -E 'accepted|rejected|no filter|unable')"
test "$out" == "$want" || {
echo "want: $want"
echo "got : $out"
exit 1
}
}
base=http://www.example.com
why "$base/x.zip: rejected by rule #2 (-*.zip)" \
--why "$base/x.zip" "$base/" '+*.png' '-*.zip'
why "$base/a.png: accepted by rule #1 (+*.png)" \
--why "$base/a.png" "$base/" '+*.png' '-*.zip'
why "$base/i.html: no filter rule matches; the wizard decides (same-site links are followed by default)" \
--why "$base/i.html" "$base/" '+*.png' '-*.zip'
# scheme-less input gets the same http:// default as primary URLs
why "$base/x.zip: rejected by rule #2 (-*.zip)" \
--why "www.example.com/x.zip" "$base/" '+*.png' '-*.zip'
# the rule latest in the list decides
why "$base/x.zip: accepted by rule #3 (+*x.zip)" \
--why "$base/x.zip" "$base/" '+*.png' '-*.zip' '+*x.zip'

View File

@@ -0,0 +1,21 @@
#!/bin/bash
#
# #180: raw non-ASCII hrefs must reach the wire UTF-8 percent-encoded whatever
# the declared page charset; each PDF exists only at its exact UTF-8 path.
set -euo pipefail
: "${top_srcdir:=..}"
bash "$top_srcdir/tests/local-crawl.sh" --errors 0 --rerun \
--found 'charset/header/统计大数据服务平台.pdf' \
--found 'charset/meta5/统计大数据服务平台.pdf' \
--found 'charset/metaeq/统计大数据服务平台.pdf' \
--found 'charset/none/统计大数据服务平台.pdf' \
--found 'charset/latin1hdr/统计大数据服务平台.pdf' \
--found 'charset/latin1real/café.pdf' \
--found 'charset/metalatin1/déjà.pdf' \
--found 'charset/latin1ovl/À¡x.pdf' \
--found 'charset/priority/nuée.pdf' \
--found 'charset/preenc/统计大数据服务平台.pdf' \
--found 'charset/bom/统计大数据服务平台.pdf' \
httrack 'BASEURL/charset/index.html'

View File

@@ -0,0 +1,21 @@
#!/bin/bash
#
# -M byte cap under a slow server (#77): p0 overruns -M at once while p1..p3
# trickle for a minute; the cap must abort the in-flight transfers, not wait
# them out. Unfixed, the smooth stop drains the trickle at server pace.
set -euo pipefail
: "${top_srcdir:=..}"
start=$(date +%s)
bash "$top_srcdir/tests/local-crawl.sh" \
--log-found 'More than 400000 bytes have been transferred.. giving up' \
httrack 'BASEURL/bigtrickle/index.html' -M400000 -c4
wall=$(($(date +%s) - start))
# trickle runs 60s; a smooth-only stop waits it out, the hard stop lands near 8s.
# Wall clock is the discriminating check: the log line above fires either way.
if [ "$wall" -ge 30 ]; then
echo "crawl took ${wall}s, -M hard stop did not engage" >&2
exit 1
fi

View File

@@ -0,0 +1,24 @@
#!/bin/bash
#
# -M hard-abort must not destroy already-complete files (#77 follow-up).
# Pass 1 mirrors bigtrunc fully. Pass 2 re-fetches under --update -M: fast.bin
# overruns the cap, its abort tears down slow.bin's stalled re-fetch. Both were
# complete on disk from pass 1, and both must survive the aborted update:
# - slow.bin's re-fetch is incomplete; the direct-to-disk write must not be
# left truncated (the previous copy is restored).
# - fast.bin fully arrives but is torn down; the aborted slot must not overwrite
# it with an empty body.
# Unfixed, slow.bin is truncated and fast.bin is zeroed while the cache still
# records them complete.
set -euo pipefail
: "${top_srcdir:=..}"
bash "$top_srcdir/tests/local-crawl.sh" \
--rerun-args '--update -M400000' \
--log-found 'More than 400000 bytes have been transferred.. giving up' \
--found bigtrunc/slow.bin \
--file-min-bytes bigtrunc/slow.bin 655360 \
--file-min-bytes bigtrunc/fast.bin 655360 \
httrack 'BASEURL/bigtrunc/index.html' -c2

View File

@@ -0,0 +1,13 @@
#!/bin/bash
# Issue #176: on --update a page that 200'd on the first crawl but now 403s must
# keep its good local copy, not be overwritten by the error body nor purged.
set -euo pipefail
: "${top_srcdir:=..}"
bash "$top_srcdir/tests/local-crawl.sh" --rerun \
--found 'errmask/keep.dat' \
--file-min-bytes 'errmask/keep.dat' 1024 \
--file-matches 'errmask/keep.dat' '^KEEP' \
--file-not-matches 'errmask/keep.dat' 'error 403' \
httrack 'BASEURL/errmask/index.html'

View File

@@ -35,8 +35,10 @@ TESTS = \
01_engine-entities.test \
01_engine-filelist.test \
01_engine-filter.test \
01_engine-filterdual.test \
01_engine-ftp-line.test \
01_engine-ftp-userpass.test \
01_engine-cacheindex.test \
01_engine-hashtable.test \
01_engine-header.test \
01_engine-idna.test \
@@ -66,6 +68,7 @@ TESTS = \
01_zlib-acceptencoding.test \
01_zlib-cache.test \
01_zlib-cache-corrupt.test \
01_zlib-cache-legacy.test \
01_zlib-cache-golden.test \
01_zlib-cache-writefail.test \
01_zlib-savename-cached.test \
@@ -105,6 +108,11 @@ TESTS = \
36_local-bigcrawl.test \
37_local-cache-outage.test \
38_local-update-304.test \
39_local-delayed-cancel.test
39_local-delayed-cancel.test \
40_local-why.test \
41_local-utf8-link.test \
42_local-maxsize-slow.test \
43_local-update-truncate.test \
44_local-update-errormask.test
CLEANFILES = check-network_sh.cache

View File

@@ -13,15 +13,21 @@
#
# Usage:
# bash local-crawl.sh [--tls] [--root DIR] [--cookie NAME=VALUE ...] \
# --errors N --files N --found PATH ... --directory PATH ... \
# [--rerun-args 'ARGS'] \
# --errors N --errors-content N --files N --found PATH ... --directory PATH ... \
# --log-found REGEX ... --log-not-found REGEX ... \
# --file-matches PATH REGEX ... --file-not-matches PATH REGEX ... \
# --max-mirror-bytes N \
# --file-min-bytes PATH N --max-mirror-bytes N \
# httrack BASEURL/some/path [httrack-args...]
# --errors counts every "Error:" log line; --errors-content drops transient
# network failures (codes -2..-6) that flake on busy loopback under -c8.
# --log-found/--log-not-found grep (ERE) the crawl's hts-log.txt.
# --max/--min-mirror-bytes bound the mirrored content bytes (host root).
# --file-matches/--file-not-matches grep (ERE) a mirrored file (PATH under the
# host root), to assert rewritten link/content survived the crawl.
# --file-min-bytes asserts a mirrored file (PATH) is at least N bytes.
# --rerun-args runs a second pass (same server and mirror dir) with the given
# extra httrack args appended, e.g. an --update run under a cap.
# --cookie writes a Netscape cookies.txt (scoped to the discovered host:port,
# which the ephemeral port forces into the cookie domain) and passes it to
# httrack via --cookies-file, to exercise preloaded cookies.
@@ -39,6 +45,7 @@ key="${testdir}/server.key"
tls=
verbose=
rerun=
rerun_args=
rerun_dead=
tmpdir=
serverpid=
@@ -122,7 +129,11 @@ while test "$pos" -lt "$nargs"; do
pos=$((pos + 1))
cookies+=("${args[$pos]}")
;;
--errors | --files)
--rerun-args)
pos=$((pos + 1))
rerun_args="${args[$pos]}"
;;
--errors | --errors-content | --files)
audit+=("${args[$pos]}" "${args[$((pos + 1))]}")
pos=$((pos + 1))
;;
@@ -130,7 +141,7 @@ while test "$pos" -lt "$nargs"; do
audit+=("${args[$pos]}" "${args[$((pos + 1))]}")
pos=$((pos + 1))
;;
--file-matches | --file-not-matches)
--file-matches | --file-not-matches | --file-min-bytes)
audit+=("${args[$pos]}" "${args[$((pos + 1))]}" "${args[$((pos + 2))]}")
pos=$((pos + 2))
;;
@@ -241,6 +252,25 @@ if test -n "$rerun"; then
fi
fi
# --- optional second pass with extra args (e.g. an --update run under a cap) ---
# Same server and mirror dir as the first pass, so the second pass sees the
# cache the first pass wrote. Used to exercise re-fetch/update behaviour.
if test -n "$rerun_args"; then
read -ra extra <<<"$rerun_args"
info "re-running httrack with ${rerun_args}"
httrack -O "$out" --user-agent="httrack $ver local ($(uname -omrs))" \
"${moreargs[@]}" "${hts[@]}" "${extra[@]}" >"${log}.2" 2>&1 &
crawlpid=$!
wait "$crawlpid"
crawlres=$?
crawlpid=
test "$crawlres" -eq 0 || ! result "second pass exited $crawlres" || {
cat "${log}.2" >&2
exit 1
}
result "OK (second pass)"
fi
# --- optional dead pass: server stopped, the cache must survive the rollback --
if test -n "$rerun_dead"; then
zip="${out}/hts-cache/new.zip"
@@ -306,6 +336,14 @@ while test "$i" -lt "${#audit[@]}"; do
assert_equals "checking errors" "${audit[$i]}" \
"$(grep -iEc "^[0-9:]*[[:space:]]Error:" "${out}/hts-log.txt")"
;;
--errors-content)
i=$((i + 1))
total=$(grep -icE "^[0-9:]*[[:space:]]Error:" "${out}/hts-log.txt")
# transient network failures (statuscode -2..-6) flake on busy loopback;
# the code parens are followed by " at link" or " after N retries at link"
transient=$(grep -cE '\(-[2-6]\) (at link|after )' "${out}/hts-log.txt" || true)
assert_equals "checking content errors" "${audit[$i]}" "$((total - transient))"
;;
--files)
i=$((i + 1))
nFiles=$(grep -E "^HTTrack Website Copier/[^ ]* mirror complete in " "${out}/hts-log.txt" |
@@ -388,6 +426,16 @@ while test "$i" -lt "${#audit[@]}"; do
exit 1
else result "OK"; fi
;;
--file-min-bytes)
path="${audit[$((i + 1))]}"
i=$((i + 2))
sz=$(wc -c <"${hostroot}/${path}" 2>/dev/null | tr -d '[:space:]')
info "checking ${path} size ${sz:-0} >= ${audit[$i]} bytes"
if test -n "$sz" && test "$sz" -ge "${audit[$i]}"; then result "OK"; else
result "file too small (or missing)"
exit 1
fi
;;
esac
i=$((i + 1))
done

View File

@@ -374,6 +374,7 @@ def big_index(port):
'<img src="/big/a/d1/d2/d3/d4/d5/d6/d7/d8/deep.png">'
'<a href="/big/f1/long.html?x=%s">long</a>'
'<a href="/big/f1/gzok.html">gzok</a>'
'<a href="/big/f1/gzid.html">gzid</a>'
'<a href="//127.0.0.1:%d/big/f1/protorel.html">protorel</a>'
'<a href="http://127.0.0.1:%d/big/f1/abshost.html">abshost</a>'
'<a href="/big/e/404.html">e404</a>'
@@ -399,6 +400,7 @@ BIG_SIMPLE_PAGES = {
"/big/f1/dir/": "dir index",
"/big/f1/long.html": "long",
"/big/f1/gzok.html": "gzok",
"/big/f1/gzid.html": "gzid",
"/big/f1/protorel.html": "protorel",
"/big/f1/abshost.html": "abshost",
"/big/f5/dw.html": "dw target",
@@ -660,6 +662,97 @@ class Handler(SimpleHTTPRequestHandler):
def route_intl_page(self):
self.send_raw(b"<html><body>accented page</body></html>\n", "text/html")
# Raw non-ASCII href matrix (#180): each variant declares the page charset
# differently; the PDF exists only at its exact UTF-8 path, so a
# mis-decoded link 404s.
CHARSET_CJK = "统计大数据服务平台.pdf"
# variant -> (index Content-Type, <head> bytes, href bytes, pdf name)
CHARSET_VARIANTS = {
"header": ("text/html; charset=utf-8", b"", CHARSET_CJK.encode(), CHARSET_CJK),
"meta5": (
"text/html",
b'<meta charset="utf-8">',
CHARSET_CJK.encode(),
CHARSET_CJK,
),
"metaeq": (
"text/html",
b'<meta http-equiv="Content-Type" content="text/html; charset=utf-8">',
CHARSET_CJK.encode(),
CHARSET_CJK,
),
"none": ("text/html", b"", CHARSET_CJK.encode(), CHARSET_CJK),
"latin1hdr": (
"text/html; charset=iso-8859-1",
b"",
CHARSET_CJK.encode(),
CHARSET_CJK,
),
# genuine latin-1 href: the charset conversion must still apply
"latin1real": (
"text/html; charset=iso-8859-1",
b"",
"café.pdf".encode("latin-1"),
"café.pdf",
),
# latin-1 declared by META only: the meta parser is load-bearing
"metalatin1": (
"text/html",
b'<meta charset="iso-8859-1">',
"déjà.pdf".encode("latin-1"),
"déjà.pdf",
),
# latin-1 bytes that form an overlong UTF-8 shape: strict validation
# must still convert them
"latin1ovl": (
"text/html; charset=iso-8859-1",
b"",
"À¡x.pdf".encode("latin-1"),
"À¡x.pdf",
),
# header wins over meta: latin-1 href only resolves if iso-8859-1 is kept
"priority": (
"text/html; charset=iso-8859-1",
b'<meta charset="utf-8">',
"nuée.pdf".encode("latin-1"),
"nuée.pdf",
),
"preenc": ("text/html", b"", quote(CHARSET_CJK).encode(), CHARSET_CJK),
"bom": ("text/html", b"", CHARSET_CJK.encode(), CHARSET_CJK),
}
def route_charset(self):
path = unquote(urlsplit(self.path).path)
parts = path.split("/")
if path == "/charset/index.html":
self.send_html(
"".join(
'\t<a href="%s/index.html">%s</a>\n' % (v, v)
for v in self.CHARSET_VARIANTS
)
)
return
if len(parts) == 4 and parts[2] in self.CHARSET_VARIANTS:
ctype, head, href, pdf = self.CHARSET_VARIANTS[parts[2]]
if parts[3] == "index.html":
body = (
b"<html><head>"
+ head
+ b'</head><body><a href="'
+ href
+ b'">doc</a></body></html>'
)
if parts[2] == "bom":
body = b"\xef\xbb\xbf" + body
self.send_raw(body, ctype)
return
if parts[3] == pdf:
self.send_raw(self.FAKE_PDF, "application/pdf")
return
self.send_response(404)
self.send_header("Content-Length", "0")
self.end_headers()
# resume / 416 loop (#206): the first GET stalls after a prefix so the crawl
# can be interrupted (partial + temp-ref); every later request is 416.
RESUME_PREFIX = b"PARTIAL-" + b"x" * 4096 # flushed before the stall
@@ -841,6 +934,33 @@ class Handler(SimpleHTTPRequestHandler):
def route_mini304_page(self):
self.big_send(b"<html><body>tiny cacheable page</body></html>\n", "text/html")
# --- /errmask/: issue #176 — a page that 200'd on the first crawl but 403s
# on the update fetch must keep its good copy, not be overwritten nor purged.
ERRMASK_GOOD = b"KEEP" + b"." * 1020 # 1024 B distinctive non-HTML body
ERRMASK_ERR = b"<html><body>error 403</body></html>\n"
def route_errmask_index(self):
self.send_html('\t<a href="keep.dat">keep</a>\n')
def route_errmask_keep(self):
# First crawl (no validator) gets the 1024 B body + Last-Modified; the
# update sends a conditional and gets a 403 error page.
if self.headers.get("If-Modified-Since") or self.headers.get("If-None-Match"):
self.send_response(403, "Forbidden")
self.send_header("Content-Type", "text/html")
self.send_header("Content-Length", str(len(self.ERRMASK_ERR)))
self.end_headers()
if self.command != "HEAD":
self.wfile.write(self.ERRMASK_ERR)
return
self.send_response(200)
self.send_header("Content-Type", "application/octet-stream")
self.send_header("Last-Modified", BIG_LASTMOD)
self.send_header("Content-Length", str(len(self.ERRMASK_GOOD)))
self.end_headers()
if self.command != "HEAD":
self.wfile.write(self.ERRMASK_GOOD)
# --- delayed-type degenerate paths (issues #5/#107) --------------------
def route_delayed_index(self):
self.send_html(
@@ -942,6 +1062,48 @@ class Handler(SimpleHTTPRequestHandler):
def route_bigfile(self):
self.send_raw(b"x" * self.BIGFILE_BYTES, "application/octet-stream")
# -M under a slow server (#77): p0 is a fast 640KB file that alone overruns
# -M; p1..p3 trickle for a minute. The cap must abort those in-flight
# transfers, not wait them out.
def route_bigtrickle_index(self):
self.send_html(
"".join('\t<a href="p%d.bin">p%d</a>\n' % (i, i) for i in range(4))
)
# -M hard-abort must not destroy an already-complete file (#77 follow-up).
# "fast.bin" alone overruns -M and completes; "slow.bin" transfers fully on
# its first fetch (initial mirror) but stalls on every later fetch (the
# --update re-fetch), so the -M abort tears it down mid-body. An engine that
# truncates the good local copy on the aborted re-fetch loses data.
slow_seen = 0
def route_bigtrunc_index(self):
self.send_html('\t<a href="fast.bin">fast</a>\n\t<a href="slow.bin">slow</a>\n')
def route_bigtrunc_slow(self):
self.send_response(200)
self.send_header("Content-Type", "application/octet-stream")
self.send_header("Content-Length", str(self.BIGFILE_BYTES))
self.end_headers()
if self.command == "HEAD":
return
# Count body fetches only, so a stray HEAD can't shift which pass stalls.
Handler.slow_seen += 1
first = Handler.slow_seen == 1
try:
if first:
self.wfile.write(b"x" * self.BIGFILE_BYTES)
self.wfile.flush()
else:
self.wfile.write(b"x" * 4096)
self.wfile.flush()
for _ in range(120):
self.wfile.write(b"x")
self.wfile.flush()
time.sleep(1.0)
except OSError:
pass
ROUTES = {
"/cookies/entrance.php": route_entrance,
"/cookies/second.php": route_second,
@@ -1014,6 +1176,14 @@ class Handler(SimpleHTTPRequestHandler):
"/bigfiles/p5.bin": route_bigfile,
"/bigfiles/p6.bin": route_bigfile,
"/bigfiles/p7.bin": route_bigfile,
"/bigtrickle/index.html": route_bigtrickle_index,
"/bigtrickle/p0.bin": route_bigfile,
"/bigtrickle/p1.bin": route_trickle_page,
"/bigtrickle/p2.bin": route_trickle_page,
"/bigtrickle/p3.bin": route_trickle_page,
"/bigtrunc/index.html": route_bigtrunc_index,
"/bigtrunc/fast.bin": route_bigfile,
"/bigtrunc/slow.bin": route_bigtrunc_slow,
"/delayed/noloc.php": route_delayed_noloc,
"/delayed/selfloop.php": route_delayed_selfloop,
"/delayed/redir.php": route_delayed_redir,
@@ -1034,6 +1204,8 @@ class Handler(SimpleHTTPRequestHandler):
"/redir/target.html": route_redir_target,
"/mini304/index.html": route_mini304_index,
"/mini304/page.html": route_mini304_page,
"/errmask/index.html": route_errmask_index,
"/errmask/keep.dat": route_errmask_keep,
}
# --- /big/ seeded pseudo-site ------------------------------------------
@@ -1079,6 +1251,13 @@ class Handler(SimpleHTTPRequestHandler):
"text/html",
extra=[("Content-Encoding", "gzip")],
)
elif path == "/big/f1/gzid.html":
# Plain body mislabeled as gzip: identity fallback keeps it (#47)
self.big_send(
body,
"text/html",
extra=[("Content-Encoding", "gzip")],
)
else:
self.big_send(body, "text/html")
elif path == "/big/f1/list.html":
@@ -1194,6 +1373,9 @@ class Handler(SimpleHTTPRequestHandler):
if path.startswith("/big/"):
self.route_big()
return True
if path.startswith("/charset/"):
self.route_charset()
return True
# Match percent-encoded paths (accented #157 route) by their decoded form.
handler = self.ROUTES.get(path) or self.ROUTES.get(unquote(path))
if handler is not None: