Compare commits

..

3 Commits

Author SHA1 Message Date
Xavier Roche
0bea390973 Add HTTRACK_DEBUG_RESOLVE and a deterministic connect-fallback test
Exercising the connect fallback needs a host that resolves to a dead address
first and a live one next, deterministically and offline. A true SYN
black-hole can't be simulated without root, but a refused address can.

HTTRACK_DEBUG_RESOLVE="host:ip[,ip...]" pins a host's resolution to a fixed
address list (curl --resolve style), reusing the PR1 resolver seam: an
addrinfo backend that synthesizes the listed addresses for the named host and
delegates other hosts to libc (copying into its own allocations so one
freeaddrinfo frees both). It is a debug/test hook, inert unless the env var is
set, and IPv6-build-only like the rest of the resolver list.

The new local crawl test binds the server to 127.0.0.1 and resolves a host to
127.0.0.2 (refused) then 127.0.0.1: the mirror only succeeds via the fallback.
A V6_SUPPORT substitution (mirroring HTTPS_SUPPORT) lets it skip on non-IPv6
builds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-06-22 20:43:51 +02:00
Xavier Roche
67af1c2f0b Fall back to the next address when a connect fails or stalls
A slot connected to a single resolved address and waited the full slot
timeout (default 120s) if that address was dead -- a blackholed IPv6 on a
dual-stack host would stall the whole mirror. With the cache now holding the
full address list, retry the next address instead of failing.

In back_wait, a connecting slot probes the resolved address count once, then:
on a refused/failed connect (a new SO_ERROR check at connect completion, since
a failed non-blocking connect is reported writable too) it falls back
immediately; on a stalled connect it falls back after a short per-candidate
deadline (min(timeout, 10s)) rather than the full timeout. The last candidate
keeps the full timeout, so single-address hosts are unchanged. Per-slot state
(current index, count, connect start) lives in struct_back, parallel to the
slot array -- no htsblk/lien_back layout change, so the ABI is untouched.

back_connect_fallback_due() (the deadline decision) and newhttp_addr()'s
address selection are unit-tested through the DNS mock.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-06-22 20:37:28 +02:00
Xavier Roche
542d6a56b5 Resolve hosts to multiple addresses and cache the full list
The DNS cache kept a single address per host and the resolver copied only
the head of getaddrinfo's result, discarding the rest. That leaves no
fallback when the chosen address is unreachable (e.g. a blackholed IPv6 on a
dual-stack host) -- the root of the "stuck on connect" stalls.

Widen t_dnscache to hold up to HTS_MAXADDRNUM addresses in resolver order and
walk ai_next when resolving. New hts_dns_resolve_all() exposes the list;
hts_dns_resolve2() still returns the first address, so existing callers are
unchanged. newhttp_addr() connects to a chosen candidate index (newhttp() is
the index-0 wrapper), for the connect-fallback path added next.

No ABI change: t_dnscache is engine-internal (httrackp holds only a pointer;
no plugin reads its fields) and the htsblk/lien_back layout is untouched.

The DNS self-test now covers the list path: count, resolver order, the
family filter, and clamping past HTS_MAXADDRNUM.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-06-22 19:19:48 +02:00
51 changed files with 1178 additions and 2379 deletions

View File

@@ -232,42 +232,30 @@ jobs:
deb:
name: deb package (lintian)
runs-on: ubuntu-24.04
# Build and gate inside Debian sid, the upload target. A Debian dpkg-deb
# produces archive-legal xz members (an Ubuntu host defaults to zstd, which
# the archive's lintian rejects), and sid's lintian carries the same
# data-driven checks (embedded-lib fingerprints and the like) the buildds and
# UDD apply -- so issues surface here instead of after upload.
container: debian:sid
steps:
- name: Install packaging toolchain
run: |
set -euo pipefail
apt-get update
apt-get install -y --no-install-recommends \
ca-certificates git \
build-essential autoconf automake libtool autoconf-archive \
zlib1g-dev libssl-dev \
debhelper devscripts lintian fakeroot
- uses: actions/checkout@v6
with:
submodules: recursive
- name: Install packaging toolchain
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 \
debhelper devscripts lintian fakeroot
# --unsigned: CI has no GPG key (also skips the release sig/checksums).
# mkdeb builds every package then runs the lintian gate (--fail-on=error,
# warning); debuild runs the packaged test pass.
# debuild builds every package, then lintian gates on errors.
#
# DEB_BUILD_OPTIONS trims work CI does not need (release builds via
# mkdeb.sh are untouched): noautodbgsym drops the -dbgsym packages whose
# LTO payloads are slow to compress and that CI never ships; parallel uses
# every core.
- name: Build and lint Debian packages
# every core. We let debuild run its test pass -- the only one now that
# mkdeb no longer runs its own -- so CI exercises the packaged tests.
- name: Build Debian packages
run: |
set -euo pipefail
# The workspace volume is owned by the host runner uid, but the
# container runs as root, so mkdeb's git calls (superproject and the
# coucal submodule) trip "dubious ownership"; mark them all safe.
git config --global --add safe.directory "*"
export DEB_BUILD_OPTIONS="noautodbgsym parallel=$(nproc)"
bash tools/mkdeb.sh --unsigned --no-release-artifacts

View File

@@ -33,9 +33,8 @@ the operational checklist: toolchain, invariants, and how to ship a change.
- Be terse. Comment the why, in English; translate French comments you touch.
- Strip AI tells from prose (em-dash overuse, rule-of-three, filler, vague
attributions). Ref: Wikipedia "Signs of AI writing". Claude Code: `/humanizer`.
- Behavior change → add a test. Fast path: a hidden `httrack -#test=NAME` engine
self-test (registry in `htsselftest.c`; `-#test` lists them) driven by a
`tests/NN_*.test`, over a slow crawl.
- Behavior change → add a test. Fast path: a hidden `httrack -#N` debug
subcommand (`htscoremain.c`) driven by a `tests/NN_*.test`, over a slow crawl.
## Review your change adversarially (strongly suggested)
Before pushing, and when reviewing others, don't skim for bugs:

View File

@@ -1,8 +1,3 @@
# The shared libraries ship without a versioned symbols control file (ABI is
# tracked via the SONAME plus a >= upstream-version dependency, see debian/rules).
libhttrack3: no-symbols-control-file usr/lib/*
# Bundled, locally patched minizip (src/minizip): it adds a zipFlush() API the
# system libminizip lacks (htscache.c flushes the cache .zip so an interrupted
# crawl leaves a valid archive), plus Android/old-zlib portability fixes.
libhttrack3: embedded-library *libminizip*

View File

@@ -1,3 +0,0 @@
# Statically linked against httrack's bundled, patched minizip (see src/minizip
# and libhttrack3's override): the zipFlush() API is absent from the system one.
proxytrack: embedded-library *libminizip*

View File

@@ -3,7 +3,7 @@
.\"
.\" This file is generated by man/makeman.sh; do not edit by hand.
.\" SPDX-License-Identifier: GPL-3.0-or-later
.TH httrack 1 "26 June 2026" "httrack website copier"
.TH httrack 1 "13 June 2026" "httrack website copier"
.SH NAME
httrack \- offline browser : copy websites to a local directory
.SH SYNOPSIS
@@ -313,8 +313,12 @@ debug HTTP headers in logfile (\-\-debug\-headers)
.SS Guru options: (do NOT use if possible)
.IP \-#X
*use optimized engine (limited memory boundary checks) (\-\-fast\-engine)
.IP \-#test
list engine self\-tests (run one with \-#test=NAME [args])
.IP \-#0
filter test (\-#0 '*.gif' 'www.bar.com/foo.gif') (\-\-debug\-testfilters <param>)
.IP \-#1
simplify test (\-#1 ./foo/bar/../foobar)
.IP \-#2
type test (\-#2 /foo/bar.php)
.IP \-#C
cache list (\-#C '*.com/spider*.gif' (\-\-debug\-cache <param>)
.IP \-#R

View File

@@ -56,7 +56,7 @@ whttrackrundir = $(bindir)
whttrackrun_SCRIPTS = webhttrack
libhttrack_la_SOURCES = htscore.c htsparse.c htsback.c htscache.c \
htscache_selftest.c htsdns_selftest.c htsselftest.c \
htscache_selftest.c htsdns_selftest.c \
htscatchurl.c htsfilters.c htsftp.c htshash.c coucal/coucal.c \
htshelp.c htslib.c htscoremain.c \
htsname.c htsrobots.c htstools.c htswizard.c \
@@ -66,7 +66,7 @@ libhttrack_la_SOURCES = htscore.c htsparse.c htsback.c htscache.c \
md5.c \
minizip/ioapi.c minizip/mztools.c minizip/unzip.c minizip/zip.c \
hts-indextmpl.h htsalias.h htsback.h htsbase.h htssafe.h \
htsbasenet.h htsbauth.h htscache.h htscache_selftest.h htsdns_selftest.h htsselftest.h htscatchurl.h \
htsbasenet.h htsbauth.h htscache.h htscache_selftest.h htsdns_selftest.h htscatchurl.h \
htsconfig.h htscore.h htsparse.h htscoremain.h htsdefines.h \
htsfilters.h htsftp.h htsglobal.h htshash.h coucal/coucal.h \
htshelp.h htsindex.h htslib.h htsmd5.h \

View File

@@ -57,10 +57,7 @@ Please visit our Website: http://www.httrack.com
// DOS
#include <process.h> /* _beginthread, _endthread */
#endif
#include <io.h> /* _chsize_s */
#define HTS_FTRUNCATE(fp, sz) _chsize_s(_fileno(fp), (sz))
#else
#define HTS_FTRUNCATE(fp, sz) ftruncate(fileno(fp), (sz))
#endif
#define VT_CLREOL "\33[K"
@@ -3766,27 +3763,7 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
}
#endif
/********** **************************** ********** */
}
// MIME type excluded by a -mime: filter: abort, don't fetch
// the body (#58)
else if (HTTP_IS_OK(back[i].r.statuscode) &&
!back[i].testmode &&
strnotempty(back[i].r.contenttype) &&
hts_acceptmime(opt, 0, back[i].url_adr,
back[i].url_fil,
back[i].r.contenttype) == 1) {
deletehttp(&back[i].r);
back[i].r.soc = INVALID_SOCKET;
back[i].status = STATUS_READY;
back_set_finished(sback, i);
back[i].r.statuscode = STATUSCODE_EXCLUDED;
strcpybuff(back[i].r.msg, "Excluded by MIME type filter");
hts_log_print(
opt, LOG_NOTICE,
"File excluded by MIME type filter (%s): %s%s",
back[i].r.contenttype, back[i].url_adr,
back[i].url_fil);
} else { // il faut aller le chercher
} else { // il faut aller le chercher
// effacer buffer (requète)
if (!noFreebuff) {
@@ -3797,70 +3774,35 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
// xxc SI CHUNK VERIFIER QUE CA MARCHE??
if (back[i].r.statuscode == 206) { // on nous envoie un morceau (la fin) coz une partie sur disque!
off_t sz = fsize_utf8(back[i].url_sav);
/* RFC 7233: resume at the server's Content-Range start,
not the offset we requested; a server may resume
earlier and appending the overlap duplicates bytes
(#198). */
const LLint resume = back[i].r.crange_start;
const hts_boolean range_ok =
back[i].r.crange > 0 && resume >= 0 &&
resume <= (LLint) sz &&
back[i].r.crange_end + 1 == back[i].r.crange &&
(back[i].r.totalsize < 0 ||
back[i].r.totalsize ==
back[i].r.crange_end - resume + 1);
#if HDEBUG
printf("partial content: " LLintP " on disk..\n",
(LLint) sz);
#endif
if (sz >= 0 && range_ok) {
if (sz >= 0) {
if (!is_hypertext_mime(opt, back[i].r.contenttype, back[i].url_sav)) { // pas HTML
if (opt->getmode & HTS_GETMODE_NONHTML) {
filenote(&opt->state.strc, back[i].url_sav, NULL); // noter fichier comme connu
file_notify(opt, back[i].url_adr, back[i].url_fil,
back[i].url_sav, 0, 1,
back[i].r.notmodified);
back[i].r.out =
FOPEN(fconv(catbuff, sizeof(catbuff),
back[i].url_sav),
"r+b"); // resume in place
back[i].r.out = FOPEN(fconv(catbuff, sizeof(catbuff), back[i].url_sav), "ab"); // append
if (back[i].r.out && opt->cache != 0) {
back[i].r.is_write = 1;
back[i].r.size = resume; // bytes already on disk
back[i].r.statuscode = HTTP_OK; // force 'OK'
back[i].r.is_write = 1; // écrire
back[i].r.size = sz; // déja écrit
back[i].r.statuscode = HTTP_OK; // Forcer 'OK'
if (back[i].r.totalsize >= 0)
back[i].r.totalsize += resume; // -> full size
// drop bytes past the resume point; a silent
// failure could leave a stale tail, so on error
// drop the partial and refetch the whole file
if (HTS_FTRUNCATE(back[i].r.out,
(off_t) resume) != 0) {
fclose(back[i].r.out);
back[i].r.out = NULL;
url_savename_refname_remove(
opt, back[i].url_adr, back[i].url_fil);
UNLINK(back[i].url_sav);
back[i].status = STATUS_READY;
back_set_finished(sback, i);
strcpybuff(back[i].r.msg,
"Can not truncate partial file, "
"restarting");
} else {
fseeko(back[i].r.out, (off_t) resume, SEEK_SET);
/* create a temporary reference file in case of
* broken mirror */
if (back_serialize_ref(opt, &back[i]) != 0) {
hts_log_print(opt, LOG_WARNING,
"Could not create temporary "
"reference file for %s%s",
back[i].url_adr,
back[i].url_fil);
}
#if HDEBUG
printf("continue interrupted file\n");
#endif
back[i].r.totalsize += sz; // plus en fait
fseek(back[i].r.out, 0, SEEK_END); // à la fin
/* create a temporary reference file in case of broken mirror */
if (back_serialize_ref(opt, &back[i]) != 0) {
hts_log_print(opt, LOG_WARNING,
"Could not create temporary reference file for %s%s",
back[i].url_adr, back[i].url_fil);
}
#if HDEBUG
printf("continue interrupted file\n");
#endif
} else { // On est dans la m**
back[i].status = STATUS_READY; // terminé (voir plus loin)
back_set_finished(sback, i);
@@ -3872,18 +3814,17 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
FILE *fp =
FOPEN(fconv(catbuff, sizeof(catbuff), back[i].url_sav), "rb");
if (fp) {
LLint alloc_mem = resume + 1;
LLint alloc_mem = sz + 1;
if (back[i].r.totalsize >= 0)
alloc_mem += back[i].r.totalsize; // AJOUTER RESTANT!
if (deleteaddr(&back[i].r)
&& (back[i].r.adr =
(char *) malloct((size_t) alloc_mem))) {
back[i].r.size = resume;
back[i].r.size = sz;
if (back[i].r.totalsize >= 0)
back[i].r.totalsize += resume; // -> full size
if ((fread(back[i].r.adr, 1, (size_t) resume,
fp)) != (size_t) resume) {
back[i].r.totalsize += sz; // plus en fait
if ((fread(back[i].r.adr, 1, sz, fp)) != sz) {
back[i].status = STATUS_READY; // terminé (voir plus loin)
back_set_finished(sback, i);
strcpybuff(back[i].r.msg,
@@ -3901,30 +3842,14 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
"No memory for partial file");
}
fclose(fp);
} else { // open failed
} else { // Argh..
back[i].status = STATUS_READY; // terminé (voir plus loin)
back_set_finished(sback, i);
strcpybuff(back[i].r.msg,
"Can not open partial file");
}
}
} else if (sz >=
0) { // unusable range -> restart whole file
hts_log_print(opt, LOG_WARNING,
"Unusable partial-content range for %s%s "
"(have " LLintP " bytes, got " LLintP
"-" LLintP "/" LLintP "), restarting",
back[i].url_adr, back[i].url_fil,
(LLint) sz, back[i].r.crange_start,
back[i].r.crange_end, back[i].r.crange);
url_savename_refname_remove(opt, back[i].url_adr,
back[i].url_fil);
UNLINK(back[i].url_sav);
back[i].status = STATUS_READY;
back_set_finished(sback, i);
strcpybuff(back[i].r.msg,
"Unusable partial content, restarting");
} else { // partial not found
} else { // Non trouvé??
back[i].status = STATUS_READY; // terminé (voir plus loin)
back_set_finished(sback, i);
strcpybuff(back[i].r.msg, "Can not find partial file");
@@ -4005,6 +3930,7 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
}
}
}
/*} */

View File

@@ -146,8 +146,7 @@ typedef enum BackStatusCode {
STATUSCODE_NON_FATAL = -5,
STATUSCODE_SSL_HANDSHAKE = -6,
STATUSCODE_TOO_BIG = -7,
STATUSCODE_TEST_OK = -10,
STATUSCODE_EXCLUDED = -11 /* aborted: MIME excluded by a -mime: filter */
STATUSCODE_TEST_OK = -10
} BackStatusCode;
/** HTTrack status ('status' member of of 'lien_back') **/

View File

@@ -220,25 +220,6 @@ struct cache_back_zip_entry {
} \
} while(0)
/* A cache (new.zip) write failed: storage is gone (disk full / dropped share),
so the mirror is doomed too. Abort it via exit_xh, don't crash as assertf
did. */
static void cache_zip_write_failed(httrackp *opt, cache_back *cache,
const char *what, int zErr) {
if (!cache->zipWriteFailed) {
cache->zipWriteFailed = HTS_TRUE;
if (check_fatal_io_errno()) {
hts_log_print(opt, LOG_ERROR,
"Mirror aborted: disk full or filesystem problems");
} else {
hts_log_print(opt, LOG_ERROR,
"Mirror aborted: cache write failed (%s): %s", what,
hts_get_zerror(zErr));
}
}
opt->state.exit_xh = -1; /* fatal: stop the mirror, exit non-zero */
}
/* Ajout d'un fichier en cache */
void cache_add(httrackp * opt, cache_back * cache, const htsblk * r,
const char *url_adr, const char *url_fil, const char *url_save,
@@ -255,10 +236,6 @@ void cache_add(httrackp * opt, cache_back * cache, const htsblk * r,
const char *url_save_suffix = url_save;
int zErr;
/* already failed and aborting; don't touch the broken stream again */
if (cache->zipWriteFailed)
return;
// robots.txt hack
if (url_save == NULL) {
dataincache = 0; // testing links
@@ -369,8 +346,9 @@ void cache_add(httrackp * opt, cache_back * cache, const htsblk * r,
*/
headers, (uInt) strlen(headers), NULL, 0, NULL, /* comment */
Z_DEFLATED, Z_DEFAULT_COMPRESSION)) != Z_OK) {
cache_zip_write_failed(opt, cache, "opening a cache entry", zErr);
return;
int zip_zipOpenNewFileInZip_failed = 0;
assertf(zip_zipOpenNewFileInZip_failed);
}
/* Write data in cache */
@@ -380,8 +358,9 @@ void cache_add(httrackp * opt, cache_back * cache, const htsblk * r,
if ((zErr =
zipWriteInFileInZip((zipFile) cache->zipOutput, r->adr,
(int) r->size)) != Z_OK) {
cache_zip_write_failed(opt, cache, "writing to the cache", zErr);
return;
int zip_zipWriteInFileInZip_failed = 0;
assertf(zip_zipWriteInFileInZip_failed);
}
}
} else {
@@ -402,10 +381,9 @@ void cache_add(httrackp * opt, cache_back * cache, const htsblk * r,
if ((zErr =
zipWriteInFileInZip((zipFile) cache->zipOutput, buff,
(int) nl)) != Z_OK) {
cache_zip_write_failed(opt, cache, "writing to the cache",
zErr);
fclose(fp);
return;
int zip_zipWriteInFileInZip_failed = 0;
assertf(zip_zipWriteInFileInZip_failed);
}
}
} while(nl > 0);
@@ -419,14 +397,16 @@ void cache_add(httrackp * opt, cache_back * cache, const htsblk * r,
/* Close */
if ((zErr = zipCloseFileInZip((zipFile) cache->zipOutput)) != Z_OK) {
cache_zip_write_failed(opt, cache, "closing a cache entry", zErr);
return;
int zip_zipCloseFileInZip_failed = 0;
assertf(zip_zipCloseFileInZip_failed);
}
/* Flush */
if ((zErr = zipFlush((zipFile) cache->zipOutput)) != 0) {
cache_zip_write_failed(opt, cache, "flushing the cache", zErr);
return;
int zip_zipFlush_failed = 0;
assertf(zip_zipFlush_failed);
}
}

View File

@@ -47,7 +47,6 @@ Please visit our Website: http://www.httrack.com
#include "htslib.h"
#include "htszlib.h"
#include <errno.h>
#include <stdio.h>
#include <string.h>
@@ -317,136 +316,6 @@ static int disk_fallback_selftest(httrackp *opt) {
return fail;
}
typedef struct {
size_t budget; /**< bytes allowed through before writes start failing */
int fail_errno; /**< errno set on the failing write (ENOSPC, EIO, ...) */
int writes; /**< zwrite call count, to detect re-entry into the stream */
} writefail_inject;
/* zwrite that copies until the budget runs out, then fails with inj->fail_errno
(the #174/#219 condition). Counts calls so the test can prove a flagged cache
never re-enters the stream. */
static uLong selftest_failing_zwrite(voidpf opaque, voidpf stream,
const void *buf, uLong size) {
writefail_inject *inj = (writefail_inject *) opaque;
inj->writes++;
if (inj->budget >= (size_t) size) {
inj->budget -= (size_t) size;
return (uLong) fwrite(buf, 1, (size_t) size, (FILE *) stream);
}
errno = inj->fail_errno;
return 0; /* short write -> the minizip op returns an error */
}
/* Open a ZIP whose writes fail past inj->budget, so cache_add() hits an error.
*/
static zipFile selftest_open_failing_zip(const char *path,
writefail_inject *inj) {
zlib_filefunc_def ff;
fill_fopen_filefunc(&ff); /* real fopen/read/seek/close; ignores opaque */
ff.zwrite_file = selftest_failing_zwrite;
ff.opaque = inj;
return zipOpen2(path, APPEND_STATUS_CREATE, NULL, &ff);
}
/* Store one octet-stream body into `cache` (all-in-cache, body in the ZIP). */
static void writefail_store(httrackp *opt, cache_back *cache, const char *fil,
const char *body, size_t body_len) {
htsblk r;
char locbuf[4];
char *bodycopy = malloct(body_len);
hts_init_htsblk(&r);
r.statuscode = 200;
r.size = (LLint) body_len;
strcpybuff(r.msg, "OK");
strcpybuff(r.contenttype, "application/octet-stream");
locbuf[0] = '\0';
r.location = locbuf;
r.is_write = 0;
memcpy(bodycopy, body, body_len);
r.adr = bodycopy;
cache_add(opt, cache, &r, "example.com", fil, "example.com/blob.bin", 1,
NULL);
freet(bodycopy);
}
/* #174/#219: a failing cache write used to crash via assertf(); it must instead
stop the mirror (exit_xh = -1) without crashing. Assert that, plus the cache
is flagged and a sibling write doesn't re-enter the broken stream. */
int cache_write_failure_selftest(httrackp *opt, const char *dir) {
int fail = 0;
char path[HTS_URLMAXSIZE];
/* incompressible + big, so deflate flushes (and fails) mid-write, before
* close */
static const size_t body_len = 256 * 1024;
char *body = malloct(body_len);
int phase;
gen_body(body, body_len, 1 /* incompressible */);
fconcat(path, sizeof(path), dir, "/wfail.zip");
/* phase 0: fail on the body write, fatal errno (ENOSPC, the disk-full
branch). phase 1: fail on the open, non-fatal errno (EIO, dropped-share
branch). Both must abort the mirror. */
for (phase = 0; phase < 2; phase++) {
cache_back cache;
writefail_inject inj;
int writes_after_fail;
inj.budget = (phase == 0) ? 4096 : 0;
inj.fail_errno = (phase == 0) ? ENOSPC : EIO;
inj.writes = 0;
memset(&cache, 0, sizeof(cache));
cache.type = 1;
cache.log = stderr;
cache.errlog = stderr;
cache.hashtable = coucal_new(0);
cache.zipOutput = selftest_open_failing_zip(path, &inj);
if (cache.zipOutput == NULL) {
fprintf(stderr, "cache-writefail: could not open injected ZIP\n");
fail++;
continue;
}
opt->state.exit_xh = 0; /* clear; the failing write must set it to -1 */
writefail_store(opt, &cache, "/blob.bin", body, body_len);
if (!cache.zipWriteFailed) {
fprintf(stderr, "cache-writefail: phase %d: write error not caught\n",
phase);
fail++;
}
if (opt->state.exit_xh != -1) {
fprintf(stderr,
"cache-writefail: phase %d: mirror not aborted (exit_xh=%d)\n",
phase, opt->state.exit_xh);
fail++;
}
/* a flagged cache must no-op a sibling write: no further backend write */
writes_after_fail = inj.writes;
writefail_store(opt, &cache, "/blob2.bin", body, 16);
if (inj.writes != writes_after_fail) {
fprintf(stderr,
"cache-writefail: phase %d: sibling write re-entered the broken "
"stream (%d extra backend writes)\n",
phase, inj.writes - writes_after_fail);
fail++;
}
if (cache.zipOutput != NULL) {
zipClose(cache.zipOutput,
NULL); /* best-effort; may fail on the backend */
cache.zipOutput = NULL;
}
}
freet(body);
return fail;
}
int cache_selftests(httrackp *opt, const char *dir) {
int failures = 0;
cache_back cache;

View File

@@ -52,10 +52,6 @@ int cache_selftests(httrackp *opt, const char *dir);
committed file, never by the test). Returns the failed-check count. */
int cache_golden_selftest(httrackp *opt, const char *dir, int regen);
/* #174/#219: assert a failing cache write aborts the mirror cleanly instead of
crashing. Returns the failed-check count. */
int cache_write_failure_selftest(httrackp *opt, const char *dir);
#endif
#endif

View File

@@ -736,39 +736,26 @@ int httpmirror(char *url1, httrackp * opt) {
/* OPTIMIZED for fast load */
if (StringNotEmpty(opt->filelist)) {
char *filelist_buff = NULL;
size_t filelist_sz = 0;
const char *filelist_err = NULL; /* failure reason, NULL on success */
const off_t fs = fsize(StringBuff(opt->filelist));
const size_t filelist_sz = off_t_to_size_t(fsize(StringBuff(opt->filelist)));
if (fs < 0) {
/* fsize() hides the cause; redo stat() for a precise errno (#49) */
struct stat st;
filelist_err = stat(StringBuff(opt->filelist), &st) != 0
? strerror(errno)
: "not a regular file";
} else if ((filelist_sz = off_t_to_size_t(fs)) == (size_t) -1) {
filelist_err = "file too large";
filelist_sz = 0;
} else {
if (filelist_sz != (size_t) -1) {
FILE *fp = fopen(StringBuff(opt->filelist), "rb");
if (fp == NULL) {
filelist_err = strerror(errno);
} else {
if (fp) {
filelist_buff = malloct(filelist_sz + 1);
if (filelist_buff == NULL) {
filelist_err = "out of memory";
} else if (fread(filelist_buff, 1, filelist_sz, fp) != filelist_sz) {
freet(filelist_buff);
filelist_err = "read error";
} else {
filelist_buff[filelist_sz] = '\0';
if (filelist_buff) {
if (fread(filelist_buff, 1, filelist_sz, fp) != filelist_sz) {
freet(filelist_buff);
filelist_buff = NULL;
} else {
*(filelist_buff + filelist_sz) = '\0';
}
}
fclose(fp);
}
}
if (filelist_buff != NULL) {
if (filelist_buff) {
int filelist_ptr = 0;
int n = 0;
char BIGSTK line[HTS_URLMAXSIZE * 2];
@@ -793,8 +780,8 @@ int httpmirror(char *url1, httrackp * opt) {
// Free buffer
freet(filelist_buff);
} else {
hts_log_print(opt, LOG_ERROR, "Could not include URL list \"%s\": %s",
StringBuff(opt->filelist), filelist_err);
hts_log_print(opt, LOG_ERROR, "Could not include URL list: %s",
StringBuff(opt->filelist));
}
}

View File

@@ -214,8 +214,6 @@ struct cache_back {
cache_back_zip_entry *zipEntries;
int zipEntriesOffs;
int zipEntriesCapa;
hts_boolean
zipWriteFailed; /**< a cache write failed; stop touching the stream */
};
#ifndef HTS_DEF_FWSTRUCT_hash_struct

View File

@@ -45,7 +45,9 @@ Please visit our Website: http://www.httrack.com
#include "htsmodules.h"
#include "htszlib.h"
#include "htscharset.h"
#include "htsselftest.h"
#include "htsencoding.h"
#include "htscache_selftest.h"
#include "htsdns_selftest.h"
#include "htsmd5.h"
#include <ctype.h>
@@ -112,6 +114,442 @@ HTSEXT_API int hts_main(int argc, char **argv) {
return ret;
}
// very minimalistic internal tests
static void basic_selftests(void) {
// BUG 756328
const char *const source = "/intent/tweet?url=https%3A%2F%2Fwww.httrack.com%2Fvacatures%2F1562519%2Fmedewerker-data-services&text=Medewerker+Data+Services&via=httrackcom";
char buffer[1024];
fil_normalized(source, buffer);
// MD5 selftests
md5selftest();
// cookie_get field extraction (tab-separated, 0-based)
{
char cbuf[8192];
assertf(strcmp(cookie_get(cbuf, "a\tb\tc", 0), "a") == 0);
assertf(strcmp(cookie_get(cbuf, "a\tb\tc", 1), "b") == 0);
assertf(strcmp(cookie_get(cbuf, "a\tb\tc", 2), "c") == 0);
// multi-char fields catch length/boundary bugs that 1-char fields hide
assertf(strcmp(cookie_get(cbuf, "host\tx\t/path/to", 0), "host") == 0);
assertf(strcmp(cookie_get(cbuf, "host\tx\t/path/to", 2), "/path/to") == 0);
assertf(strcmp(cookie_get(cbuf, "a\t\tc", 1), "") == 0); // empty field
assertf(strcmp(cookie_get(cbuf, "a\tb\tc", 9), "") == 0); // beyond last
}
// back_infostr() status-line formatting (no sockets: pure formatting over
// in-memory slots). Stresses a few thousand entries across every status-code
// arm. Regression for a clobber bug where the size/totalsize trailer was
// written straight into the destination, wiping the URL it had just built.
{
static const struct {
int code;
const char *tag;
} cases[] = {
{200, "READY "}, {-1, "ERROR "}, {-2, "TIMEOUT "},
{-3, "TOOSLOW "}, {400, "BADREQUEST "}, {403, "FORBIDDEN "},
{404, "NOT FOUND "}, {500, "SERVERROR "}, {999, "ERROR(999)"},
};
const int ncases = (int) (sizeof(cases) / sizeof(cases[0]));
const int n = 2000;
lien_back *slots = calloct(n, sizeof(lien_back));
char line[HTS_URLMAXSIZE * 4 + 1024];
char expect[HTS_URLMAXSIZE * 4 + 1024];
struct_back sb;
int idx;
sb.lnk = slots;
sb.count = n;
sb.ready = NULL;
sb.ready_size_bytes = 0;
for (idx = 0; idx < n; idx++) {
lien_back *const slot = &slots[idx];
slot->r.location = slot->location_buffer;
slot->status = STATUS_READY;
slot->r.statuscode = cases[idx % ncases].code;
slot->r.size = idx;
slot->r.totalsize = idx + 1;
snprintf(slot->url_adr, sizeof(slot->url_adr), "http://h%d.example", idx);
snprintf(slot->url_fil, sizeof(slot->url_fil), "/p/%d.html", idx);
}
for (idx = 0; idx < n; idx++) {
line[0] = '\0';
back_infostr(&sb, idx, 3, line, sizeof(line));
// Exact match (not substring): pins tag/URL/trailer order and rejects a
// partial clobber, duplication, or truncation that a presence check would
// let through. The expected format is stated here independently.
snprintf(expect, sizeof(expect),
"%s\"http://h%d.example/p/%d.html\" " LLintP " " LLintP " ",
cases[idx % ncases].tag, idx, idx, (LLint) idx,
(LLint) (idx + 1));
assertf(strcmp(line, expect) == 0);
}
// Near-maximal URL, driven through back_info() (which owns the status
// buffer internally and prints to a FILE*). url_adr + url_fil together
// overrun the old HTS_URLMAXSIZE*2+1024 buffer, so the bounded appends
// would abort unless that buffer is sized to hold both fields. Regression
// for that sizing -- exercising back_infostr() directly would miss it,
// since the caller's buffer is what matters.
{
lien_back *const slot = &slots[0];
const size_t adrlen = sizeof(slot->url_adr) - 8;
const size_t fillen = sizeof(slot->url_fil) - 8;
FILE *const fp = tmpfile();
size_t got;
assertf(fp != NULL);
slot->status = STATUS_READY;
slot->r.statuscode = 200;
slot->r.size = 1;
slot->r.totalsize = 2;
memset(slot->url_adr, 'a', adrlen);
slot->url_adr[adrlen] = '\0';
slot->url_fil[0] = '/';
memset(slot->url_fil + 1, 'b', fillen - 1);
slot->url_fil[fillen] = '\0';
back_info(&sb, 0, 3, fp);
rewind(fp);
got = fread(line, 1, sizeof(line) - 1, fp);
line[got] = '\0';
fclose(fp);
snprintf(expect, sizeof(expect),
"READY \"%s%s\" " LLintP " " LLintP " " LF, slot->url_adr,
slot->url_fil, (LLint) 1, (LLint) 2);
assertf(strcmp(line, expect) == 0);
}
freet(slots);
}
// next_token(): in-place token scanner. Strips surrounding quotes, unescapes
// \" and \\ when flag is set, and returns the token terminator (the space, or
// NULL at end of string). The unquote/unescape rewrites the string in place
// by shifting left, so the result is always shorter -- regression for that
// compaction.
{
char tok[64];
// plain token: unchanged, returns a pointer AT the separating space (exact
// position, not just any space -- a strchr-style impl would land elsewhere
// once quotes shift the content)
strcpybuff(tok, "abc def");
{
char *const end = next_token(tok, 0);
assertf(end == tok + 3 && *end == ' ' && strcmp(tok, "abc def") == 0);
}
// surrounding quotes stripped, returns the (post-shift) trailing space
strcpybuff(tok, "\"ab\" cd");
{
char *const end = next_token(tok, 1);
assertf(end == tok + 2 && *end == ' ' && strcmp(tok, "ab cd") == 0);
}
// a space inside quotes does not end the token; end of string returns NULL
strcpybuff(tok, "\"a b\"c");
{
char *const end = next_token(tok, 1);
assertf(end == NULL && strcmp(tok, "a bc") == 0);
}
// \" and \\ are unescaped to literal " and \ in place
strcpybuff(tok, "\"a\\\"b\\\\c\"");
{
char *const end = next_token(tok, 1);
assertf(end == NULL && strcmp(tok, "a\"b\\c") == 0);
}
// unterminated quote: the opening quote is dropped, the rest survives, and
// the scan runs to the NUL (returns NULL)
strcpybuff(tok, "\"ab");
{
char *const end = next_token(tok, 1);
assertf(end == NULL && strcmp(tok, "ab") == 0);
}
// trailing lone backslash in a quote: *(p+1) is the NUL, not an escape, so
// the backslash is kept intact (and there is no over-read past the NUL)
strcpybuff(tok, "\"a\\");
{
char *const end = next_token(tok, 1);
assertf(end == NULL && strcmp(tok, "a\\") == 0);
}
}
// fil_normalized(): canonicalizes a URL path. Query arguments are sorted
// alphabetically (by the text after each '?'/'&') and the query is rebuilt
// through a bounded builder; outside the query, "//" collapses to "/".
// Regression for that builder.
{
char norm[256];
assertf(strcmp(fil_normalized("/p?b=2&a=1&c=3", norm), "/p?a=1&b=2&c=3") ==
0);
assertf(strcmp(fil_normalized("/a//b", norm), "/a/b") == 0);
// "//" is collapsed only before the query; inside the query it is kept
assertf(strcmp(fil_normalized("/a//b?x=c//d", norm), "/a/b?x=c//d") == 0);
}
// give_mimext(): mime type -> file extension, bounded into the caller buffer.
// Returns 1 when an extension was written, 0 otherwise.
{
char ext[16];
assertf(give_mimext(ext, sizeof(ext), "image/gif") == 1);
assertf(strcmp(ext, "gif") == 0);
assertf(give_mimext(ext, sizeof(ext), "text/html") == 1);
assertf(strcmp(ext, "html") == 0);
assertf(give_mimext(ext, sizeof(ext), "no/such-mime-type") == 0);
assertf(ext[0] == '\0');
}
// convtolower(): lower-cases into the caller buffer (bounded by its size).
{
char low[64];
assertf(strcmp(convtolower(low, sizeof(low), "ABC/Def.HTML"),
"abc/def.html") == 0);
}
// cut_path(): splits a path into directory (with trailing '/') and basename,
// each bounded by its buffer size.
{
char path[256];
char pname[256];
{
char full[] = "/dir/sub/file.html";
cut_path(full, path, sizeof(path), pname, sizeof(pname));
assertf(strcmp(path, "/dir/sub/") == 0);
assertf(strcmp(pname, "file.html") == 0);
}
{ // a trailing slash is trimmed before the split
char full[] = "/dir/sub/";
cut_path(full, path, sizeof(path), pname, sizeof(pname));
assertf(strcmp(path, "/dir/") == 0);
assertf(strcmp(pname, "sub") == 0);
}
{ // a path of length <= 1 yields empty results
char full[] = "/";
cut_path(full, path, sizeof(path), pname, sizeof(pname));
assertf(path[0] == '\0' && pname[0] == '\0');
}
}
// get_httptype_sized(): a long MIME type (Office OOXML reaches 73 chars) is
// written whole into a contenttype-sized buffer; returns 1 on a match, 0 when
// flag==0 and nothing matched. Regression for the old contenttype[64]
// overflow.
{
httrackp *opt = hts_create_opt();
htsblk r; // write into the real struct field, not a stand-in
assertf(opt != NULL);
// a long MIME (Office OOXML reaches 73 chars) must fit htsblk.contenttype
// whole: a [64] field would make this bounded copy abort.
assertf(get_httptype_sized(opt, r.contenttype, sizeof(r.contenttype),
"deck.pptx", 0) == 1);
assertf(strcmp(r.contenttype,
"application/vnd.openxmlformats-officedocument."
"presentationml.presentation") == 0);
assertf(get_httptype_sized(opt, r.contenttype, sizeof(r.contenttype),
"x.gif", 0) == 1);
assertf(strcmp(r.contenttype, "image/gif") == 0);
// no extension and flag==0: nothing written, returns 0
assertf(get_httptype_sized(opt, r.contenttype, sizeof(r.contenttype),
"noextfile", 0) == 0);
assertf(r.contenttype[0] == '\0');
// no extension and flag==1: octet-stream fallback, returns 1
assertf(get_httptype_sized(opt, r.contenttype, sizeof(r.contenttype),
"noextfile", 1) == 1);
assertf(strcmp(r.contenttype, "application/octet-stream") == 0);
// a user --assume rule with an empty value matches but writes nothing:
// get_userhttptype returns 1 with the buffer empty, so get_httptype_sized
// must still report 0 (callers test the return like the old
// strnotempty(s)).
StringCopy(opt->mimedefs, "\ncgi=\n");
assertf(get_httptype_sized(opt, r.contenttype, sizeof(r.contenttype),
"/x.cgi", 0) == 0);
assertf(r.contenttype[0] == '\0');
StringCopy(opt->mimedefs, "\ncgi=text/html\n");
assertf(get_httptype_sized(opt, r.contenttype, sizeof(r.contenttype),
"/x.cgi", 0) == 1);
assertf(strcmp(r.contenttype, "text/html") == 0);
hts_free_opt(opt);
}
// adr_normalized_sized(): bounded host normalization (passthrough when
// already normal).
{
char n[HTS_URLMAXSIZE];
assertf(strcmp(adr_normalized_sized("example.com", n, sizeof(n)),
"example.com") == 0);
}
// standard_name(): builds "<name><md5?>.<ext>" into a bounded buffer. The md5
// is appended (4 chars) only when the URL has a query string (see url_md5),
// so test both; pin the structure (name + ext, lengths), not the md5 chars.
{
char b[HTS_URLMAXSIZE * 2];
const char *nom = "index.html"; // name part
const char *dot = nom + 5; // points at ".html"
size_t len;
// no query -> no md5: "index" + ".html"
standard_name(b, sizeof(b), dot, nom, "http://example.com/index.html", 0);
assertf(strcmp(b, "index.html") == 0);
// query -> 4 md5 chars between name and ext: "index" + md5(4) + ".html"
standard_name(b, sizeof(b), dot, nom, "http://example.com/index.html?v=1",
0);
len = strlen(b);
assertf(len == 5 + 4 + 5);
assertf(strncmp(b, "index", 5) == 0);
assertf(strcmp(b + len - 5, ".html") == 0);
// short names: name kept (<=8), the extension is clamped to 3 -> ".htm"
standard_name(b, sizeof(b), dot, nom, "http://example.com/index.html?v=1",
1);
len = strlen(b);
assertf(len == 5 + 4 + 4);
assertf(strcmp(b + len - 4, ".htm") == 0);
// short names with a >8-char name: the name is clamped to 8 ("indexpag")
{
const char *lnom = "indexpage.html";
const char *ldot = lnom + 9; // points at ".html"
standard_name(b, sizeof(b), ldot, lnom,
"http://example.com/indexpage.html?v=1", 1);
len = strlen(b);
assertf(len == 8 + 4 + 4);
assertf(strncmp(b, "indexpag", 8) == 0);
assertf(strcmp(b + len - 4, ".htm") == 0);
}
}
// longfile_to_83(): single-name 8-3 (mode 1) / ISO9660 (mode 2) conversion;
// uppercases, clamps the name (8 / 31) and the extension (3). It rewrites
// 'save' in place, so pass a mutable array.
{
char n83[256];
{
char save[] = "longfilename.html";
longfile_to_83(1, n83, sizeof(n83), save); // 8-3: name->8, ext->3
assertf(strcmp(n83, "LONGFILE.HTM") == 0);
}
{
char save[] = "longfilename.html";
longfile_to_83(2, n83, sizeof(n83), save); // ISO9660: name->31, ext->3
assertf(strcmp(n83, "LONGFILENAME.HTM") == 0);
}
{ // sanitization: leading '.'->'_', interior dots
char save[] = ".a b.c.d e"; // collapse to '_', spaces/specials -> '_'
// (only the last dot stays as the separator)
longfile_to_83(1, n83, sizeof(n83), save);
assertf(strcmp(n83, "_A_B_C.D_E") == 0);
}
}
// long_to_83(): per-segment 8-3 conversion of a whole path.
{
char n83[HTS_URLMAXSIZE * 2];
char save[] = "dir/longfilename.html";
long_to_83(1, n83, sizeof(n83), save);
assertf(strcmp(n83, "DIR/LONGFILE.HTM") == 0);
}
// lienrelatif(): relative path from the directory of curr_fil to link.
{
char s[HTS_URLMAXSIZE * 2];
// same directory -> just the basename
assertf(lienrelatif(s, sizeof(s), "dir/page.html", "dir/index.html") == 0);
assertf(strcmp(s, "page.html") == 0);
// link one level up -> a "../" prefix
assertf(lienrelatif(s, sizeof(s), "a.html", "dir/index.html") == 0);
assertf(strcmp(s, "../a.html") == 0);
}
}
/* Self-tests for the htssafe.h bounded string ops (driven by httrack -#8).
Returns 0 if every bounded operation behaved correctly, 1 otherwise.
The abort-on-overflow guarantee is checked separately by the -#8 "overflow"
sub-mode (it aborts the process by design). */
static int string_safety_selftests(void) {
char buf[8];
/* strcpybuff into a sized array: exact copy */
strcpybuff(buf, "abc");
if (strcmp(buf, "abc") != 0)
return 1;
/* strcatbuff append within capacity */
strcatbuff(buf, "de");
if (strcmp(buf, "abcde") != 0)
return 1;
/* strncatbuff appends at most N source chars */
strcpybuff(buf, "ab");
strncatbuff(buf, "cdef", 2);
if (strcmp(buf, "abcd") != 0)
return 1;
/* strlcpybuff: explicit-capacity copy into a pointer destination, the form
the migration moves toward */
{
char storage[8];
char *const p = storage;
strlcpybuff(p, "hello", sizeof(storage));
if (strcmp(p, "hello") != 0)
return 1;
}
/* strcpybuff into a pointer destination: routes through the unchecked
strcpybuff_ptr_ fallback (the path the -#8 warning flags). The warning is
intentional here; we only verify the fallback still copies correctly. */
#if defined(__GNUC__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wattribute-warning"
#endif
{
char storage[8];
char *const p = storage;
strcpybuff(p, "ptr");
if (strcmp(p, "ptr") != 0)
return 1;
}
#if defined(__GNUC__)
#pragma GCC diagnostic pop
#endif
/* htsbuff: bounded builder over a fixed array (append, truncating append,
reset, and length tracking) */
{
char dst[8];
htsbuff b = htsbuff_array(dst);
htsbuff_cat(&b, "ab");
htsbuff_cat(&b, "cd");
if (strcmp(htsbuff_str(&b), "abcd") != 0 || b.len != 4)
return 1;
htsbuff_catn(&b, "efghij", 2); /* append at most 2 */
if (strcmp(htsbuff_str(&b), "abcdef") != 0)
return 1;
htsbuff_cpy(&b, "xyz"); /* reset */
if (strcmp(htsbuff_str(&b), "xyz") != 0 || b.len != 3)
return 1;
htsbuff_catc(&b, '!'); /* single character */
if (strcmp(htsbuff_str(&b), "xyz!") != 0 || b.len != 4)
return 1;
}
/* boundary: filling to exactly cap-1 must succeed (one more aborts, which the
-#8 overflow-buff mode checks) */
{
char d2[4];
htsbuff c = htsbuff_array(d2);
htsbuff_cat(&c, "abc");
if (strcmp(htsbuff_str(&c), "abc") != 0 || c.len != 3)
return 1;
}
return 0;
}
static int hts_main_internal(int argc, char **argv, httrackp * opt);
// Main, récupère les paramètres et appelle le robot
@@ -906,25 +1344,6 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
}
*/
/* Engine self-tests: -#test lists them, -#test=NAME [args] runs one. Handled
here, ahead of the no-URL usage gate below, so they need no dummy URL. */
{
int k;
for (k = 1; k < argc; k++) {
const char *const a = argv[k];
if (a[0] == '-' && a[1] == '#' && strncmp(a + 2, "test", 4) == 0 &&
(a[6] == '\0' || a[6] == '=')) {
const char *const name = a[6] == '=' ? a + 7 : NULL;
const int code = hts_selftest(opt, name, argc - (k + 1), &argv[k + 1]);
htsmain_free();
return code;
}
}
}
// Pas d'URL
#if DEBUG_STEPS
printf("Checking URLs\n");
@@ -2013,6 +2432,42 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
case '#':{ // non documenté
com++;
switch (*com) {
case 'A': // cache self-test: httrack -#A <dir>
if (na + 1 < argc) {
const int err = cache_selftests(opt, argv[na + 1]);
printf("cache-selftest: %s\n", err ? "FAIL" : "OK");
htsmain_free();
return err;
} else {
fprintf(stderr, "Option #A requires a directory argument\n");
htsmain_free();
return 1;
}
break;
case 'B': // golden cache fixture read: httrack -#B <dir> [regen]
if (na + 1 < argc) {
const int regen =
(na + 2 < argc && strcmp(argv[na + 2], "regen") == 0);
const int err =
cache_golden_selftest(opt, argv[na + 1], regen);
printf("cache-golden: %s\n", err ? "FAIL" : "OK");
htsmain_free();
return err;
} else {
fprintf(stderr, "Option #B requires a directory argument\n");
htsmain_free();
return 1;
}
break;
case 'D': { // DNS resolver/cache self-test (mock getaddrinfo)
const int err = dns_selftests(opt);
printf("dns-selftest: %s\n", err ? "FAIL" : "OK");
htsmain_free();
return err;
} break;
case 'C': // list cache files : httrack -#C '*spid*.gif' will attempt to find the matching file
{
int hasFilter = 0;
@@ -2310,6 +2765,468 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
}
break;
case '0': /* test #0 : filters */
if (na + 2 >= argc) {
HTS_PANIC_PRINTF
("Option #0 needs to be followed by a filter string and a string");
printf("Example: '-#0' '*.gif' 'foo.gif'\n");
htsmain_free();
return -1;
} else {
if (strjoker(argv[na + 2], argv[na + 1], NULL, NULL))
printf("%s does match %s\n", argv[na + 2], argv[na + 1]);
else
printf("%s does NOT match %s\n", argv[na + 2],
argv[na + 1]);
htsmain_free();
return 0;
}
break;
case '1': /* test #1 : fil_simplifie */
if (na + 1 >= argc) {
HTS_PANIC_PRINTF("Option #1 needs to be followed by an URL");
printf("Example: '-#1' ./foo/bar/../foobar\n");
htsmain_free();
return -1;
} else {
fil_simplifie(argv[na + 1]);
printf("simplified=%s\n", argv[na + 1]);
htsmain_free();
return 0;
}
break;
case 'l': /* lienrelatif: relative link from curr_fil to link */
if (na + 2 >= argc) {
HTS_PANIC_PRINTF(
"Option #l needs a link and a current-file path");
printf(
"Example: '-#l' 'host/dir/img.gif' 'host/dir/p.html'\n");
htsmain_free();
return -1;
} else {
char s[HTS_URLMAXSIZE * 2];
if (lienrelatif(s, sizeof(s), argv[na + 1], argv[na + 2]) ==
0)
printf("relative=%s\n", s);
else
printf("relative=<ERROR>\n");
htsmain_free();
return 0;
}
break;
case 'i': /* ident_url_relatif: resolve a link -> adr/fil */
if (na + 3 >= argc) {
HTS_PANIC_PRINTF(
"Option #i needs a link, an origin address and file");
printf("Example: '-#i' '../img.gif' 'www.foo.com' "
"'/d/p.html'\n");
htsmain_free();
return -1;
} else {
lien_adrfil af;
const int r = ident_url_relatif(argv[na + 1], argv[na + 2],
argv[na + 3], &af);
if (r == 0)
printf("adr=%s fil=%s\n", af.adr, af.fil);
else
printf("error=%d\n", r);
htsmain_free();
return 0;
}
break;
case '2': // mimedefs
if (na + 1 >= argc) {
HTS_PANIC_PRINTF("Option #2 needs to be followed by an URL");
printf("Example: '-#2' /foo/bar.php\n");
htsmain_free();
return -1;
} else {
char mime[256];
// initialiser mimedefs
//get_userhttptype(opt,1,opt->mimedefs,NULL);
// check
if (get_httptype_sized(opt, mime, sizeof(mime), argv[na + 1],
0)) {
char ext[256];
printf("%s is '%s'\n", argv[na + 1], mime);
if (give_mimext(ext, sizeof(ext), mime)) {
printf("and its local type is '.%s'\n", ext);
}
} else {
printf("%s is of an unknown MIME type\n", argv[na + 1]);
}
htsmain_free();
return 0;
}
break;
case '3': // charset tests: httrack -#3 "iso-8859-1" "café"
if (++na + 1 < argc) {
char *s =
hts_convertStringToUTF8(argv[na+1], strlen(argv[na+1]), argv[na]);
if (s != NULL) {
printf("%s\n", s);
free(s);
} else {
fprintf(stderr, "invalid string for charset %s\n", argv[na]);
}
na += 2;
} else {
fprintf(stderr,
"Option #3 needs to be followed by a charset and a string");
}
htsmain_free();
return 0;
break;
case '4': // IDNA encoder: httrack -#4 "www.café.com"
if (++na < argc) {
char *s = hts_convertStringUTF8ToIDNA(argv[na], strlen(argv[na]));
if (s != NULL) {
printf("%s\n", s);
free(s);
} else {
fprintf(stderr, "invalid string '%s'\n", argv[na]);
}
na += 1;
} else {
fprintf(stderr,
"Option #4 needs to be followed by an IDNA string");
}
htsmain_free();
return 0;
break;
case '5': // IDNA encoder: httrack -#5
if (++na < argc) {
char *s = hts_convertStringIDNAToUTF8(argv[na], strlen(argv[na]));
if (s != NULL) {
printf("%s\n", s);
free(s);
} else {
fprintf(stderr, "invalid string '%s'\n", argv[na]);
}
na += 1;
} else {
fprintf(stderr,
"Option #5 needs to be followed by an IDNA string");
}
htsmain_free();
return 0;
break;
case '6': // entities: httrack -#6 "&foo;" ["encoding"]
if (++na < argc) {
char *const s = strdup(argv[na]);
const char *const enc = na + 1 < argc ? argv[na + 1] : "UTF-8";
if (s != NULL
&& hts_unescapeEntitiesWithCharset(s, s, strlen(s),
enc) == 0) {
printf("%s\n", s);
free(s);
} else {
fprintf(stderr, "invalid string '%s'\n", argv[na]);
}
na += 1;
} else {
fprintf(stderr,
"Option #6 needs to be followed by a string");
}
htsmain_free();
return 0;
break;
case '8': /* string-safety selftest: httrack -#8 [overflow <bigstr>] */
if (na + 1 < argc
&& strncmp(argv[na + 1], "overflow", 8) == 0) {
/* Deliberately exceed a sized buffer: the bounded op must
abort. The source comes from argv so its length is opaque
to the compiler (no static -Wstringop-overflow, genuine
runtime check). "overflow-buff" exercises htsbuff. */
char small[4];
const char *const src =
(na + 2 < argc) ? argv[na + 2] : "overflowing";
if (strcmp(argv[na + 1], "overflow-buff") == 0) {
htsbuff b = htsbuff_array(small);
htsbuff_cat(&b, src);
} else {
strcpybuff(small, src);
}
printf("strsafe: NOT aborted\n"); /* must be unreachable */
htsmain_free();
return 1;
} else {
const int err = string_safety_selftests();
printf("strsafe: %s\n", err ? "FAIL" : "OK");
htsmain_free();
return err;
}
break;
case '7': // hashtable selftest: httrack -#7 nb_entries
basic_selftests();
if (++na < argc) {
char *const snum = strdup(argv[na]);
unsigned long count = 0;
const char *const names[] = {
"", "add", "delete", "dry-add", "dry-del",
"test-exists", "test-not-exist"
};
const struct {
enum {
DO_END,
DO_ADD,
DO_DEL,
DO_DRY_ADD,
DO_DRY_DEL,
TEST_ADD,
TEST_DEL
} type;
size_t modulus;
size_t offset;
} bench[] = {
{ DO_ADD, 4, 0 }, /* add 4/0 */
{ TEST_ADD, 4, 0 }, /* check 4/0 */
{ TEST_DEL, 4, 1 }, /* check 4/1 */
{ TEST_DEL, 4, 2 }, /* check 4/2 */
{ TEST_DEL, 4, 3 }, /* check 4/3 */
{ DO_DRY_DEL, 4, 1 }, /* del 4/1 */
{ DO_DRY_DEL, 4, 2 }, /* del 4/2 */
{ DO_DRY_DEL, 4, 3 }, /* del 4/3 */
{ DO_ADD, 4, 1 }, /* add 4/1 */
{ DO_DRY_ADD, 4, 1 }, /* add 4/1 */
{ TEST_ADD, 4, 0 }, /* check 4/0 */
{ TEST_ADD, 4, 1 }, /* check 4/1 */
{ TEST_DEL, 4, 2 }, /* check 4/2 */
{ TEST_DEL, 4, 3 }, /* check 4/3 */
{ DO_ADD, 4, 2 }, /* add 4/2 */
{ DO_DRY_DEL, 4, 3 }, /* del 4/3 */
{ DO_ADD, 4, 3 }, /* add 4/3 */
{ DO_DEL, 4, 3 }, /* del 4/3 */
{ TEST_ADD, 4, 0 }, /* check 4/0 */
{ TEST_ADD, 4, 1 }, /* check 4/1 */
{ TEST_ADD, 4, 2 }, /* check 4/2 */
{ TEST_DEL, 4, 3 }, /* check 4/3 */
{ DO_DEL, 4, 0 }, /* del 4/0 */
{ DO_DEL, 4, 1 }, /* del 4/1 */
{ DO_DEL, 4, 2 }, /* del 4/2 */
/* empty here */
{ TEST_DEL, 1, 0 }, /* check */
{ DO_ADD, 4, 0 }, /* add 4/0 */
{ DO_ADD, 4, 1 }, /* add 4/1 */
{ DO_ADD, 4, 2 }, /* add 4/2 */
{ DO_DEL, 42, 0 }, /* add 42/0 */
{ TEST_DEL, 42, 0 }, /* check 42/0 */
{ TEST_ADD, 42, 2 }, /* check 42/2 */
{ DO_END }
};
char *buff = NULL;
const char **strings = NULL;
/* produce key #i */
#define FMT() \
char buffer[256]; \
const char *name; \
const long expected = (long) i * 1664525 + 1013904223; \
do { \
if (strings == NULL) { \
snprintf(buffer, sizeof(buffer), \
"http://www.example.com/website/sample/for/hashtable/" \
"%ld/index.html?foo=%ld&bar", \
(long) i, (long) (expected)); \
name = buffer; \
} else { \
name = strings[i]; \
} \
} while(0)
/* produce random patterns, or read from a file */
if (sscanf(snum, "%lu", &count) != 1) {
const off_t size = fsize(snum);
FILE *fp = fopen(snum, "rb");
if (fp != NULL) {
buff = malloc(size);
if (buff != NULL && fread(buff, 1, size, fp) == size) {
size_t capa = 0;
size_t i, last;
for(i = 0, last = 0, count = 0 ; i < size ; i++) {
if (buff[i] == 10 || buff[i] == 0) {
buff[i] = '\0';
if (capa == count) {
if (capa == 0) {
capa = 16;
} else {
capa <<= 1;
}
strings = (const char **) realloc((void*) strings, capa*sizeof(char*));
}
strings[count++] = &buff[last];
last = i + 1;
}
}
}
fclose(fp);
}
}
/* successfully read */
if (count > 0) {
coucal hashtable = coucal_new(0);
size_t loop;
for(loop = 0 ; bench[loop].type != DO_END ; loop++) {
size_t i;
for(i = bench[loop].offset ; i < (size_t) count
; i += bench[loop].modulus) {
int result;
FMT();
if (bench[loop].type == DO_ADD
|| bench[loop].type == DO_DRY_ADD) {
size_t k;
result = coucal_write(hashtable, name, (uintptr_t) expected);
for(k = 0 ; k < /* stash_size*2 */ 32 ; k++) {
(void) coucal_write(hashtable, name, (uintptr_t) expected);
}
/* revert logic */
if (bench[loop].type == DO_DRY_ADD) {
result = result ? 0 : 1;
}
}
else if (bench[loop].type == DO_DEL
|| bench[loop].type == DO_DRY_DEL) {
size_t k;
result = coucal_remove(hashtable, name);
for(k = 0 ; k < /* stash_size*2 */ 32 ; k++) {
(void) coucal_remove(hashtable, name);
}
/* revert logic */
if (bench[loop].type == DO_DRY_DEL) {
result = result ? 0 : 1;
}
}
else if (bench[loop].type == TEST_ADD
|| bench[loop].type == TEST_DEL) {
intptr_t value = -1;
result = coucal_readptr(hashtable, name, &value);
if (bench[loop].type == TEST_ADD && result
&& value != expected) {
fprintf(stderr, "value failed for %s (expected %ld, got %ld)\n",
name, (long) expected, (long) value);
exit(EXIT_FAILURE);
}
/* revert logic */
if (bench[loop].type == TEST_DEL) {
result = result ? 0 : 1;
}
}
if (!result) {
fprintf(stderr, "failed %s{%d/+%d} test on loop %ld"
" at offset %ld for %s\n",
names[bench[loop].type],
(int) bench[loop].modulus,
(int) bench[loop].offset,
(long) loop, (long) i, name);
exit(EXIT_FAILURE);
}
}
}
coucal_delete(&hashtable);
fprintf(stderr, "all hashtable tests were successful!\n");
} else {
fprintf(stderr, "Malformed number\n");
exit(EXIT_FAILURE);
}
#undef FMT
} else {
fprintf(stderr,
"Option #7 needs to be followed by a number");
exit(EXIT_FAILURE);
}
htsmain_free();
return 0;
break;
case '9': { // copy_htsopt selftest: httrack -#9
httrackp *from = hts_create_opt();
httrackp *to = hts_create_opt();
int err = 0;
/* from-values differ from both the to-values and the
hts_create_opt() defaults (nearlink FALSE, errpage/parseall
TRUE), so a copy that no-ops or just resets to defaults is
caught too, not only the unsigned-guard bug. */
from->retry = 7; /* int field: positive control */
to->retry = 0;
from->nearlink = HTS_TRUE;
to->nearlink = HTS_FALSE;
from->errpage = HTS_FALSE;
to->errpage = HTS_TRUE;
from->parseall = HTS_FALSE;
to->parseall = HTS_TRUE;
copy_htsopt(from, to);
if (to->retry != 7)
err = 1;
if (to->nearlink != HTS_TRUE)
err = 1;
if (to->errpage != HTS_FALSE)
err = 1;
if (to->parseall != HTS_FALSE)
err = 1;
/* HTS_DEFAULT (-1) is "unspecified": copy_htsopt must skip it,
leaving the target intact. Only a signed (int-backed) field
can hold -1, so this also guards the type against regressing
to an unsigned hts_boolean. */
from->parseall = HTS_DEFAULT;
to->parseall = HTS_TRUE;
copy_htsopt(from, to);
if (to->parseall != HTS_TRUE)
err = 1;
hts_free_opt(from);
hts_free_opt(to);
printf("copy-htsopt: %s\n", err ? "FAIL" : "OK");
htsmain_free();
return err;
} break;
case 'Q': { // cookie request-header selftest: httrack -#Q
static t_cookie cookie;
char hdr[1024];
/* RFC 6265: bare name=value pairs, no $Version/$Path (#151). */
const char *expected = "Cookie: name=value; has_js=1" H_CRLF;
int err = 0;
const char *dom = "www.example.com";
int added;
cookie.max_len = (int) sizeof(cookie.data);
cookie.data[0] = '\0';
added = cookie_add(&cookie, "name", "value", dom, "/");
added |= cookie_add(&cookie, "has_js", "1", dom, "/");
/* different domain: must be filtered out */
added |= cookie_add(&cookie, "junk", "x", "other.org", "/");
if (added) {
printf("cookie-header: FAIL (cookie_add setup)\n");
htsmain_free();
return 1;
}
http_cookie_header_selftest(&cookie, dom, "/", hdr,
sizeof(hdr));
if (strcmp(hdr, expected) != 0)
err = 1;
if (strstr(hdr, "$Version") != NULL ||
strstr(hdr, "$Path") != NULL)
err = 1;
if (strstr(hdr, "junk") != NULL) // wrong-domain cookie leaked
err = 1;
printf("cookie-header: %s\n", err ? "FAIL" : "OK");
if (err)
printf(" got: %s\n", hdr);
htsmain_free();
return err;
} break;
case '!':
HTS_PANIC_PRINTF
("Option #! is disabled for security reasons");

View File

@@ -76,8 +76,7 @@ int fa_strjoker(int type, char **filters, int nfil, const char *nom, LLint * siz
}
if (size)
sz = *size;
/* size unknown (scan time): no size pointer => size tests stay neutral */
if (strjoker(nom, filters[i] + filteroffs, size ? &sz : NULL, size_flag)) {
if (strjoker(nom, filters[i] + filteroffs, &sz, size_flag)) { // reconnu
if (size)
if (sz != *size)
sizelimit = sz;

View File

@@ -646,7 +646,9 @@ void help(const char *app, int more) {
infomsg("");
infomsg("Guru options: (do NOT use if possible)");
infomsg(" #X *use optimized engine (limited memory boundary checks)");
infomsg(" #test list engine self-tests (run one with -#test=NAME [args])");
infomsg(" #0 filter test (-#0 '*.gif' 'www.bar.com/foo.gif')");
infomsg(" #1 simplify test (-#1 ./foo/bar/../foobar)");
infomsg(" #2 type test (-#2 /foo/bar.php)");
infomsg(" #C cache list (-#C '*.com/spider*.gif'");
infomsg(" #R cache repair (damaged cache)");
infomsg(" #d debug parser");

View File

@@ -4177,10 +4177,9 @@ HTSEXT_API hts_boolean get_httptype_sized(httrackp *opt, char *s, size_t ssize,
/* Check html -> text/html */
const char *a = fil + strlen(fil) - 1;
/* a < fil when fil is empty: bound before dereferencing */
while ((a > fil) && (*a != '.') && (*a != '/'))
while((*a != '.') && (*a != '/') && (a > fil))
a--;
if (a >= fil && *a == '.' && strlen(a) < 32) {
if (*a == '.' && strlen(a) < 32) {
int j = 0;
a++;
@@ -4767,51 +4766,64 @@ int hts_read(htsblk * r, char *buff, int size) {
// -- Gestion cache DNS --
// 'RX98
// Free a DNS cache record (coucal value handler).
static void hts_cache_value_free(coucal_opaque arg, coucal_value value) {
void *record = value.ptr;
(void) arg;
freet(record);
}
// opt's DNS cache hashtable, created on first use. Records (t_dnscache*) are
// owned by the table and freed by hts_cache_value_free on coucal_delete.
coucal hts_cache(httrackp *opt) {
// 'capsule' contenant uniquement le cache
t_dnscache *hts_cache(httrackp * opt) {
assertf(opt != NULL);
if (opt->state.dns_cache == NULL) {
coucal cache = coucal_new(0);
coucal_set_name(cache, "dns_cache");
coucal_value_set_value_handler(cache, hts_cache_value_free, NULL);
opt->state.dns_cache = cache;
opt->state.dns_cache = (t_dnscache *) malloct(sizeof(t_dnscache));
memset(opt->state.dns_cache, 0, sizeof(t_dnscache));
}
assertf(opt->state.dns_cache != NULL);
/* first entry is NULL */
assertf(opt->state.dns_cache->iadr == NULL);
return opt->state.dns_cache;
}
// MUST BE LOCKED (coucal is not internally serialized vs FTP/web threads)
// Free DNS cache.
void hts_cache_free(t_dnscache *const root) {
if (root != NULL) {
t_dnscache *cache;
for(cache = root; cache != NULL; ) {
t_dnscache *const next = cache->next;
cache->next = NULL;
freet(cache);
cache = next;
}
}
}
// lock le cache dns pour tout opération d'ajout
// plus prudent quand plusieurs threads peuvent écrire dedans..
// -1: status? 0: libérer 1:locker
// MUST BE LOCKED
// Look up iadr in the DNS cache, filling out[0..min(count,max)-1].
// Returns: -1 not yet tested; 0 negative-cached (not in DNS); >0 address count.
static int hts_ghbn_all(coucal cache, const char *const iadr,
static int hts_ghbn_all(const t_dnscache *cache, const char *const iadr,
SOCaddr *const out, const int max) {
void *ptr;
assertf(out != NULL);
assertf(iadr != NULL);
if (*iadr == '\0') {
return -1;
}
if (coucal_read_pvoid(cache, iadr, &ptr)) { // ok trouvé
const t_dnscache *const record = (const t_dnscache *) ptr;
int i;
/* first entry is empty */
if (cache->iadr == NULL) {
cache = cache->next;
}
for(; cache != NULL; cache = cache->next) {
assertf(cache != NULL);
assertf(cache->iadr != NULL);
assertf(cache->iadr == (const char*) cache + sizeof(t_dnscache));
if (strcmp(cache->iadr, iadr) == 0) { // ok trouvé
int i;
assertf(record->host_count <= HTS_MAXADDRNUM);
for (i = 0; i < record->host_count && i < max; i++) {
assertf(record->host_length[i] <= sizeof(record->host_addr[i]));
SOCaddr_copyaddr2(out[i], record->host_addr[i], record->host_length[i]);
assertf(cache->host_count <= HTS_MAXADDRNUM);
for (i = 0; i < cache->host_count && i < max; i++) {
assertf(cache->host_length[i] <= sizeof(cache->host_addr[i]));
SOCaddr_copyaddr2(out[i], cache->host_addr[i], cache->host_length[i]);
}
return cache->host_count;
}
return record->host_count;
}
return -1;
}
@@ -5076,7 +5088,7 @@ static int hts_dns_resolve_list_(httrackp *opt, const char *_iadr,
SOCaddr *const out, const int max,
const char **error) {
char BIGSTK iadr[HTS_URLMAXSIZE * 2];
coucal cache = hts_cache(opt); // le cache dns
t_dnscache *cache = hts_cache(opt); // adresse du cache
int count;
assertf(opt != NULL);
@@ -5097,10 +5109,13 @@ static int hts_dns_resolve_list_(httrackp *opt, const char *_iadr,
if (count >= 0) { // cache hit (0 == negative-cached)
return count;
} else { // non présent dans le cache dns, tester
const size_t iadr_len = strlen(iadr) + 1;
SOCaddr resolved[HTS_MAXADDRNUM];
t_dnscache *record;
int i;
// find queue
for(; cache->next != NULL; cache = cache->next) ;
#if DEBUGDNS
printf("resolving (not cached) %s\n", iadr);
#endif
@@ -5111,18 +5126,22 @@ static int hts_dns_resolve_list_(httrackp *opt, const char *_iadr,
DEBUG_W("gethostbyname done\n");
#endif
/* attempt to store new entry (coucal owns it and dups the host key) */
record = malloct(sizeof(t_dnscache));
if (record != NULL) {
memset(record, 0, sizeof(*record));
record->host_count = count;
/* attempt to store new entry */
cache->next = malloct(sizeof(t_dnscache) + iadr_len);
if (cache->next != NULL) {
t_dnscache *const next = cache->next;
char *const block = (char*) cache->next;
char *const str = block + sizeof(t_dnscache);
memcpy(str, iadr, iadr_len);
next->iadr = str;
next->host_count = count;
for (i = 0; i < count; i++) {
record->host_length[i] = SOCaddr_size(resolved[i]);
assertf(record->host_length[i] <= sizeof(record->host_addr[i]));
memcpy(record->host_addr[i], &SOCaddr_sockaddr(resolved[i]),
record->host_length[i]);
next->host_length[i] = SOCaddr_size(resolved[i]);
assertf(next->host_length[i] <= sizeof(next->host_addr[i]));
memcpy(next->host_addr[i], &SOCaddr_sockaddr(resolved[i]),
next->host_length[i]);
}
coucal_add_pvoid(cache, iadr, record);
next->next = NULL;
}
/* copy result to caller (cache store may have failed; result still valid)
@@ -5993,14 +6012,14 @@ HTSEXT_API void hts_free_opt(httrackp * opt) {
/* Cache */
if (opt->state.dns_cache != NULL) {
coucal root;
t_dnscache *root;
hts_mutexlock(&opt->state.lock);
root = opt->state.dns_cache;
opt->state.dns_cache = NULL;
hts_mutexrelease(&opt->state.lock);
coucal_delete(&root); // frees records via hts_cache_value_free
hts_cache_free(root);
}
/* Cancel chain */

View File

@@ -147,8 +147,9 @@ struct OLD_htsblk {
#define HTS_DEF_FWSTRUCT_t_dnscache
typedef struct t_dnscache t_dnscache;
#endif
// One DNS cache record, stored as a coucal value keyed by hostname.
struct t_dnscache {
struct t_dnscache *next;
const char *iadr;
// resolved addresses, in resolver (RFC 6724) order; host_count==0 means the
// name does not resolve (negative cache). host_count<=HTS_MAXADDRNUM.
int host_count;
@@ -244,9 +245,8 @@ HTSEXT_API int check_hostname_dns(const char *const hostname);
int ftp_available(void);
#if HTS_DNSCACHE
/* Return opt's DNS cache hashtable (hostname -> t_dnscache record), creating it
on first use. Records are owned by the table and freed on coucal_delete. */
coucal hts_cache(httrackp *opt);
void hts_cache_free(t_dnscache *const cache);
t_dnscache *hts_cache(httrackp * opt);
#endif
// outils divers

View File

@@ -760,9 +760,9 @@ int url_savename(lien_adrfilsave *const afs,
strcatbuff(fil, DEFAULT_HTML); // nommer page par défaut (à priori ici html depuis un proxy http)
}
}
// Change the extension? e.g. php3 saved as html, cgi as html or gif/xbm
// depending on the resolved type.
if (ext_chg && !opt->no_type_change) {
// Changer extension?
// par exemple, php3 sera sauvé en html, cgi en html ou gif, xbm etc.. selon les cas
if (ext_chg && !opt->no_type_change) { // changer ext
char *a = fil + strlen(fil) - 1;
if ((opt->debug > 1) && (opt->log != NULL)) {
@@ -774,19 +774,11 @@ int url_savename(lien_adrfilsave *const afs,
adr_complete, fil_complete, ext);
}
if (ext_chg == 1) {
// Cut the old extension only when it is empty (a bare trailing dot), the
// new one, or a recognized one; an unknown trailing ".token" (e.g.
// /article-1.884291, #115) is part of the name, not an extension.
const char *const old_ext = get_ext(catbuff, sizeof(catbuff), fil);
const int known_ext = !*old_ext || strfield2(old_ext, ext) ||
is_knowntype(opt, fil) || is_dyntype(old_ext) ||
ishtml_ext(old_ext) != -1;
while((a > fil) && (*a != '.') && (*a != '/'))
a--;
if (*a == '.' && known_ext)
*a = '\0'; // cut
strcatbuff(fil, "."); // re-add the dot
if (*a == '.')
*a = '\0'; // couper
strcatbuff(fil, "."); // recopier point
} else {
while((a > fil) && (*a != '/'))
a--;
@@ -794,7 +786,7 @@ int url_savename(lien_adrfilsave *const afs,
a++;
*a = '\0';
}
strcatbuff(fil, ext); // append ext/name
strcatbuff(fil, ext); // copier ext/nom
}
// Rechercher premier / et dernier .
{
@@ -1729,10 +1721,10 @@ char *url_savename_refname_fullpath(httrackp * opt, const char *adr,
StringBuff(opt->path_log), digest_filename);
}
/* remove refname if any; HTS_TRUE if it was removed */
hts_boolean url_savename_refname_remove(httrackp *opt, const char *adr,
const char *fil) {
/* remove refname if any */
void url_savename_refname_remove(httrackp * opt, const char *adr,
const char *fil) {
char *filename = url_savename_refname_fullpath(opt, adr, fil);
return UNLINK(filename) == 0 ? HTS_TRUE : HTS_FALSE;
(void) UNLINK(filename);
}

View File

@@ -104,9 +104,8 @@ char *url_md5(char *digest_buffer, const char *fil_complete);
void url_savename_refname(const char *adr, const char *fil, char *filename);
char *url_savename_refname_fullpath(httrackp * opt, const char *adr,
const char *fil);
/* Remove the temp-ref for (adr,fil); HTS_TRUE if it was removed. */
hts_boolean url_savename_refname_remove(httrackp *opt, const char *adr,
const char *fil);
void url_savename_refname_remove(httrackp * opt, const char *adr,
const char *fil);
#endif
#endif

View File

@@ -241,7 +241,7 @@ struct htsoptstate {
char *userhttptype;
int verif_backblue_done; /**< backblue.gif/fade.gif already emitted */
int verif_external_status;
coucal dns_cache; /**< DNS resolution cache: hostname -> t_dnscache record */
t_dnscache *dns_cache; /**< DNS resolution cache */
int dns_cache_nthreads; /**< number of in-flight DNS resolver threads */
/* HTML parsing state */
char _hts_errmsg[HTS_CDLMAXSIZE + 256]; /**< last engine error message */

View File

@@ -3749,60 +3749,44 @@ int hts_mirror_check_moved(htsmoduleStruct * str,
} // bloc
// erreur HTTP (ex: 404, not found)
} else if ((r->statuscode == HTTP_PRECONDITION_FAILED) ||
(r->statuscode == HTTP_REQUESTED_RANGE_NOT_SATISFIABLE)) {
// 412/416: the resume partial is stale; re-get the whole file (#206)
lien_back *itemback = NULL;
int had_partial = 0;
int ref_existed = 0;
int ref_gone;
// Drop the temp-ref, its partial, and heap->sav so the re-get carries no
// Range; else back_add rebuilds the same Range and loops.
if (back_unserialize_ref(opt, heap(ptr)->adr, heap(ptr)->fil,
&itemback) == 0) {
had_partial = 1;
ref_existed = 1;
// best-effort: an orphaned partial cannot re-Range once the ref is gone
if (fexist_utf8(itemback->url_sav))
(void) UNLINK(fconv(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
itemback->url_sav));
back_clear_entry(itemback);
freet(itemback);
}
// don't re-record if the ref survived (it would re-Range and loop)
ref_gone =
url_savename_refname_remove(opt, heap(ptr)->adr, heap(ptr)->fil) ||
!ref_existed;
} else if ((r->statuscode == HTTP_PRECONDITION_FAILED)
|| (r->statuscode == HTTP_REQUESTED_RANGE_NOT_SATISFIABLE)
) { // Precondition Failed, c'est à dire pour nous redemander TOUT le fichier
if (fexist_utf8(heap(ptr)->sav)) {
had_partial = 1;
remove(heap(ptr)->sav);
remove(heap(ptr)->sav); // Eliminer
} else {
hts_log_print(opt, LOG_WARNING,
"Unexpected 412/416 error (%s) for %s%s, '%s' could not be found on disk",
r->msg, urladr(), urlfil(),
heap(ptr)->sav != NULL ? heap(ptr)->sav : "");
}
// Re-get once, only if a partial existed and both Range triggers are
// gone; a failed removal gives up rather than looping. range_used is
// unreliable (it does not survive the delayed-type two-pass).
if (had_partial && ref_gone && !fexist_utf8(heap(ptr)->sav)) {
if (!fexist_utf8(heap(ptr)->sav)) { // Bien éliminé? (sinon on boucle..)
#if HDEBUG
printf("Partial content NOT up-to-date, reget all file for %s\n",
heap(ptr)->sav);
#endif
hts_log_print(opt, LOG_DEBUG, "Partial file reget (%s) for %s%s",
r->msg, urladr(), urlfil());
// enregistrer le MEME lien
if (hts_record_link(opt, heap(ptr)->adr, heap(ptr)->fil, heap(ptr)->sav, "", "", NULL)) {
heap_top()->testmode = heap(ptr)->testmode;
heap_top()->link_import = 0;
heap_top()->testmode = heap(ptr)->testmode; // mode test?
heap_top()->link_import = 0; // pas mode import
heap_top()->depth = heap(ptr)->depth;
heap_top()->pass2 = max(heap(ptr)->pass2, numero_passe);
heap_top()->retry = heap(ptr)->retry;
heap_top()->premier = heap(ptr)->premier;
heap_top()->precedent = ptr;
//
// canceller lien actuel
error = 1;
hts_invalidate_link(opt, ptr); // invalidate hashtable entry
} else { // out of memory
XH_uninit;
hts_invalidate_link(opt, ptr); // invalidate hashtable entry
//
} else { // oups erreur, plus de mémoire!!
XH_uninit; // désallocation mémoire & buffers
return 0;
}
} else {
hts_log_print(opt, LOG_WARNING,
"Giving up on partial reget (%s) for %s%s", r->msg,
urladr(), urlfil());
hts_log_print(opt, LOG_ERROR, "Can not remove old file %s", urlfil());
error = 1;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,52 +0,0 @@
/* ------------------------------------------------------------ */
/*
HTTrack Website Copier, Offline Browser for Windows and Unix
Copyright (C) 2026 Xavier Roche and other contributors
SPDX-License-Identifier: GPL-3.0-or-later
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Ethical use: we kindly ask that you NOT use this software to harvest email
addresses or to collect any other private information about people. Doing so
would dishonor our work and waste the many hours we have spent on it.
Please visit our Website: http://www.httrack.com
*/
/* ------------------------------------------------------------ */
/* File: htsselftest.h */
/* named dispatch for the hidden engine self-tests */
/* Author: Xavier Roche */
/* ------------------------------------------------------------ */
#ifndef HTSSELFTEST_DEFH
#define HTSSELFTEST_DEFH
#ifdef HTS_INTERNAL_BYTECODE
#ifndef HTS_DEF_FWSTRUCT_httrackp
#define HTS_DEF_FWSTRUCT_httrackp
typedef struct httrackp httrackp;
#endif
/* Run engine self-test `name` over the positional args argv[0..argc-1], or list
the available tests when name is NULL, empty, or "list". Prints the result;
returns the process exit code (0 == success). The caller owns option cleanup.
Reached through the hidden `httrack -#test[=NAME ...]` subcommand. */
int hts_selftest(httrackp *opt, const char *name, int argc, char **argv);
#endif
#endif

View File

@@ -4,7 +4,7 @@
# POSIX /bin/sh on some platforms (e.g. macOS), so avoid bashisms and GNU-only
# tool flags despite the #!/bin/bash above.
# Golden cache-format regression test (driven by 'httrack -#test=cache-golden <dir>').
# Golden cache-format regression test (driven by 'httrack -#B <dir>').
#
# 01_engine-cache.test writes the cache with the same build it reads back (a
# round-trip), so it cannot catch a read-path or ZIP-format regression where
@@ -13,7 +13,7 @@
# byte-exact.
#
# Regenerate the fixture after a deliberate format change with
# 'httrack -#test=cache-golden <dir> regen', then copy <dir>/hts-cache/new.zip over the
# 'httrack -#B <dir> regen', then copy <dir>/hts-cache/new.zip over the
# committed file.
set -eu
@@ -37,11 +37,11 @@ trap 'rm -rf "$dir"' EXIT
mkdir -p "$dir/hts-cache"
cp "$fixture/hts-cache/new.zip" "$dir/hts-cache/new.zip"
out=$(httrack -#test=cache-golden "$dir")
out=$(httrack -#B "$dir")
# Match the exact success line: the read must have found and verified every
# entry, not merely failed to enter the mode (a renamed/removed test prints the
# registry to stderr, which also exits non-zero but never prints this).
# entry, not merely failed to enter the mode (a bad -#B falls through to the
# usage screen, which also exits non-zero but never prints this).
test "$out" = "cache-golden: OK" || {
echo "expected 'cache-golden: OK', got: $out" >&2
exit 1

View File

@@ -1,24 +0,0 @@
#!/bin/bash
#
# Keep this POSIX-portable: the harness runs it via $(BASH), which is a plain
# POSIX /bin/sh on some platforms (e.g. macOS), so avoid bashisms and GNU-only
# tool flags despite the #!/bin/bash above.
# Cache write-failure handling (httrack -#test=cache-writefail <dir>). #174/#219.
# A failing new.zip write (disk full) used to crash the process via assertf; it
# must instead stop the mirror with a fatal error (exit_xh=-1), no crash. The
# self-test asserts that; reverting the fix makes -#test=cache-writefail abort (SIGABRT) and fail.
set -eu
dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT
out=$(httrack -#test=cache-writefail "$dir")
# Match the exact success line (error logs also go to stdout); a renamed/removed
# test prints the registry to stderr, which exits non-zero but never prints this.
printf '%s\n' "$out" | grep -qx "cache-writefail: OK" || {
echo "expected 'cache-writefail: OK', got: $out" >&2
exit 1
}

View File

@@ -4,7 +4,7 @@
# POSIX /bin/sh on some platforms (e.g. macOS), so avoid bashisms and GNU-only
# tool flags despite the #!/bin/bash above.
# Cache create/read/update logic (driven by 'httrack -#test=cache <dir>').
# Cache create/read/update logic (driven by 'httrack -#A <dir>').
#
# The in-process self-test stores several hand-crafted edge entries (normal
# HTML, an empty redirect with a near-limit location, a non-HTML body kept via
@@ -20,13 +20,13 @@ set -eu
dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT
# The working directory is a required argument; without it the test prints a
# usage line to stderr and returns non-zero.
out=$(httrack -#test=cache "$dir")
# Like the other -# debug modes, a trailing token (the working directory) is
# required; a bare '-#A' falls through to the usage screen.
out=$(httrack -#A "$dir")
# Match the exact success line, so the test cannot pass for an unrelated reason
# (e.g. the cache test being gone, which prints the registry to stderr but
# never prints this line).
# (e.g. the -#A mode being gone and falling through to the usage screen, which
# also exits non-zero but never prints this).
test "$out" = "cache-selftest: OK" || {
echo "expected 'cache-selftest: OK', got: $out" >&2
exit 1

View File

@@ -4,13 +4,13 @@
set -euo pipefail
# charset -> UTF-8 conversion (hts_convertStringToUTF8).
# -#test=charset <charset> <string> prints the string re-decoded from <charset> as UTF-8.
# -#3 <charset> <string> prints the string re-decoded from <charset> as UTF-8.
conv() {
test "$(httrack -O /dev/null -#test=charset "$1" "$2")" == "$3" || exit 1
test "$(httrack -O /dev/null -#3 "$1" "$2")" == "$3" || exit 1
}
# crash probe: malformed input must exit cleanly, not abort.
runs() {
httrack -O /dev/null -#test=charset "$1" "$2" >/dev/null 2>&1 || exit 1
httrack -O /dev/null -#3 "$1" "$2" >/dev/null 2>&1 || exit 1
}
# the source bytes below are UTF-8 (this file is UTF-8); "café" is 0x63 61 66 C3 A9.
@@ -31,7 +31,7 @@ conv 'us-ascii' 'hello' 'hello'
# unknown charset: ASCII passes through unchanged, but non-ASCII input cannot be
# decoded and yields empty output (an error is printed to stderr).
conv 'no-such-charset-xyz' 'abc' 'abc'
test "$(httrack -O /dev/null -#test=charset 'no-such-charset-xyz' 'café' 2>/dev/null)" == "" || exit 1
test "$(httrack -O /dev/null -#3 'no-such-charset-xyz' 'café' 2>/dev/null)" == "" || exit 1
# malformed UTF-8 (lone continuation byte, truncated lead byte) must not crash
runs 'utf-8' $'\x80'

View File

@@ -1,15 +1,14 @@
#!/bin/bash
#
# Issue #151 guard: the request Cookie header must be bare RFC 6265 name=value
# pairs, no $Version/$Path attributes. Driven by the 'httrack -#test=cookies' selftest.
# pairs, no $Version/$Path attributes. Driven by the 'httrack -#Q' selftest.
set -eu
# 'run' is an ignored placeholder argument.
out=$(httrack -#test=cookies run)
# A trailing token is required; a bare '-#Q' falls through to the usage screen.
out=$(httrack -#Q run)
# Exact-match the success line so a renamed/removed test (it prints the registry
# to stderr) can't pass.
# Exact-match the success line so a fall-through to usage can't pass the test.
test "$out" = "cookie-header: OK" || {
echo "expected 'cookie-header: OK', got: $out" >&2
exit 1

View File

@@ -2,16 +2,15 @@
#
# Regression guard for the unsigned-enum sentinel trap: copy_htsopt's
# `if (from->X > -1)` guard is always false for unsigned hts_boolean fields, so
# they silently stop being copied. Driven by the in-process 'httrack -#test=copyopt' test.
# they silently stop being copied. Driven by the in-process 'httrack -#9' test.
# Keep POSIX-portable (harness runs it via $(BASH), a plain /bin/sh on macOS).
set -eu
# 'run' is an ignored placeholder argument.
out=$(httrack -#test=copyopt run)
# A trailing token is required; a bare '-#9' falls through to the usage screen.
out=$(httrack -#9 run)
# Exact-match the success line so a renamed/removed test (it prints the registry
# to stderr) can't pass.
# Exact-match the success line so a fall-through to usage can't pass the test.
test "$out" = "copy-htsopt: OK" || {
echo "expected 'copy-htsopt: OK', got: $out" >&2
exit 1

View File

@@ -5,8 +5,9 @@ set -euo pipefail
# DNS resolver/cache self-test: a mock getaddrinfo (no network) checks address
# family, single-address selection, the -@i4/-@i6 family filter, and cache reuse.
# 'run' is an ignored placeholder argument.
out=$(httrack -#test=dns run)
# The trailing token is required, like the other -# selftests, so a bare command
# line isn't treated as "no arguments" and routed to the usage screen.
out=$(httrack -#D run)
test "$out" = "dns-selftest: OK" || {
echo "expected 'dns-selftest: OK', got: $out" >&2

View File

@@ -4,13 +4,13 @@
set -euo pipefail
# HTML entity unescaping (hts_unescapeEntitiesWithCharset).
# -#test=entities <string> prints the string with entities decoded (UTF-8 output).
# -#6 <string> prints the string with entities decoded (UTF-8 output).
ent() {
test "$(httrack -O /dev/null -#test=entities "$1")" == "$2" || exit 1
test "$(httrack -O /dev/null -#6 "$1")" == "$2" || exit 1
}
# crash probe: malformed input must exit cleanly, not abort.
runs() {
httrack -O /dev/null -#test=entities "$1" >/dev/null 2>&1 || exit 1
httrack -O /dev/null -#6 "$1" >/dev/null 2>&1 || exit 1
}
# named entities

View File

@@ -1,65 +0,0 @@
#!/bin/bash
#
# -%L URL-list loading (#49): a readable list is honored; an unusable one fails
# with the reason (errno / not-a-regular-file), not a bare "Could not include
# URL list". Offline: file:// fixture, no server. Asserts on httrack's own
# strings and the message shape, so it is locale-independent.
set -euo pipefail
tmp=$(mktemp -d "${TMPDIR:-/tmp}/httrack_filelist.XXXXXX") || exit 1
trap 'rm -rf "$tmp"' EXIT HUP INT QUIT PIPE TERM
echo '<html><body>hi</body></html>' >"$tmp/index.html"
# run httrack with the given -%L target; structured log lands in $out/hts-log.txt
run() {
local out="$1" list="$2"
rm -rf "$out"
mkdir -p "$out"
httrack -O "$out" --quiet -n "-%L" "$list" >"$out/.stdout" 2>&1 || true
LOG="$out/hts-log.txt"
}
fail() {
echo "FAIL: $1"
cat "$LOG"
exit 1
}
loghas() {
grep -Eq "$1" "$LOG" || fail "expected /$1/ in $LOG"
}
lognot() {
if grep -Eq "$1" "$LOG"; then fail "unexpected /$1/ in $LOG"; fi
}
# readable list: its one URL is loaded and counted (count must be non-zero)
printf 'file://%s/index.html\n' "$tmp" >"$tmp/urls.txt"
run "$tmp/ok" "$tmp/urls.txt"
loghas '[1-9][0-9]* links added from'
# missing file: quoted name + a non-empty reason, never the old reasonless
# "Could not include URL list: <name>". The reason is the stat() errno, not the
# directory fallback literal (guards against dropping the errno lookup).
run "$tmp/miss" "$tmp/nope.txt"
loghas 'Could not include URL list "[^"]+": .+'
lognot 'Could not include URL list: '
lognot 'not a regular file'
# a directory is rejected with our own reason (locale-independent)
mkdir -p "$tmp/adir"
run "$tmp/dir" "$tmp/adir"
loghas 'Could not include URL list "[^"]+": not a regular file'
# unreadable regular file: the fopen() errno arm fires, distinct from the
# directory branch. Root bypasses mode 000, so skip it there.
if test "$(id -u)" -ne 0; then
: >"$tmp/noperm.txt"
chmod 000 "$tmp/noperm.txt"
run "$tmp/perm" "$tmp/noperm.txt"
chmod 644 "$tmp/noperm.txt"
loghas 'Could not include URL list "[^"]+": .+'
lognot 'not a regular file'
fi
exit 0

View File

@@ -4,13 +4,13 @@
set -euo pipefail
# wildcard filter engine (strjoker), the core of +/- include/exclude rules.
# -#test=filter <filter> <string> prints "<string> does match <filter>" or "... does NOT match ...".
# -#0 <filter> <string> prints "<string> does match <filter>" or "... does NOT match ...".
match() {
test "$(httrack -O /dev/null -#test=filter "$1" "$2")" == "$2 does match $1" || exit 1
test "$(httrack -O /dev/null -#0 "$1" "$2")" == "$2 does match $1" || exit 1
}
nomatch() {
test "$(httrack -O /dev/null -#test=filter "$1" "$2")" == "$2 does NOT match $1" || exit 1
test "$(httrack -O /dev/null -#0 "$1" "$2")" == "$2 does NOT match $1" || exit 1
}
# bare star matches everything
@@ -71,27 +71,3 @@ nomatch '*[\[\]]' '[' # not matched, despite the docs
match '*[\[\]]' ']' # only via the empty class-match + trailing ']'
match '*[\[\]]' '[]' # one of {'[','\'} then the trailing ']'
nomatch '*[\[\]]' '[]x'
# Size-based rules (-#test=filtersize <size> <string> <filter...>): a negative size
# means the size is still unknown (scan time). A size exclusion must stay neutral
# then, so the file is fetched and only cancelled once its size is known (#143).
fsize() {
local want="$1"
shift
test "$(httrack -O /dev/null -#test=filtersize "$@")" == "$want" || exit 1
}
fsize 'verdict=allowed size_flag=0' -1 foo.jpg -* '+*.jpg' '-*.jpg*[<10]' # scan time: keep
fsize 'verdict=forbidden size_flag=1' 5 foo.jpg -* '+*.jpg' '-*.jpg*[<10]' # <10KB: cancel
fsize 'verdict=allowed size_flag=1' 20 foo.jpg -* '+*.jpg' '-*.jpg*[<10]' # >=10KB: keep
fsize 'verdict=forbidden size_flag=0' -1 foo.txt -* '+*.jpg' '-*.jpg*[<10]' # not a jpg
# the '>' operator is just as neutral at scan time, and fires once size is known
fsize 'verdict=allowed size_flag=0' -1 foo.jpg -* '+*.jpg' '-*.jpg*[>10]' # scan time: keep
fsize 'verdict=forbidden size_flag=1' 20 foo.jpg -* '+*.jpg' '-*.jpg*[>10]' # >10KB: cancel
# [name]/[file]/[path] never span '?' mid-string; a trailing query is still
# tolerated by the global '?' rule (same as plain *.aspx), not the class (#144).
nomatch '*[path]/end' 'a?b/end'
nomatch '*[file]end' 'foo?xend'
nomatch '*[name]X' 'abc?X'
match '*[file]' 'foo?x=1' # trailing query: tolerated, as for *.aspx
match '*.aspx' 'page.aspx?y=2'

View File

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

View File

@@ -3,13 +3,13 @@
set -euo pipefail
# IDNA / punycode encode (-#test=idna-encode) and decode (-#test=idna-decode). This code has a CVE history,
# IDNA / punycode encode (-#4) and decode (-#5). This code has a CVE history,
# so the edge cases below cover passthrough, round-trips, and malformed input.
enc() { test "$(httrack -O /dev/null -#test=idna-encode "$1")" == "$2" || exit 1; }
dec() { test "$(httrack -O /dev/null -#test=idna-decode "$1")" == "$2" || exit 1; }
enc() { test "$(httrack -O /dev/null -#4 "$1")" == "$2" || exit 1; }
dec() { test "$(httrack -O /dev/null -#5 "$1")" == "$2" || exit 1; }
# crash probe: malformed ACE input must exit cleanly, not abort.
runs() { httrack -O /dev/null -#test=idna-decode "$1" >/dev/null 2>&1 || exit 1; }
runs() { httrack -O /dev/null -#5 "$1" >/dev/null 2>&1 || exit 1; }
# encode
enc 'www.café.com' 'www.xn--caf-dma.com'

View File

@@ -4,13 +4,13 @@
set -euo pipefail
# MIME type guessing from extension (get_httptype / give_mimext).
# -#test=mime <path> prints "<path> is '<mime>'" then "and its local type is '.<ext>'".
# -#2 <path> prints "<path> is '<mime>'" then "and its local type is '.<ext>'".
mime() {
test "$(httrack -O /dev/null -#test=mime "$1" | head -1)" == "$1 is '$2'" || exit 1
test "$(httrack -O /dev/null -#2 "$1" | head -1)" == "$1 is '$2'" || exit 1
}
unknown() {
test "$(httrack -O /dev/null -#test=mime "$1" | head -1)" == "$1 is of an unknown MIME type" || exit 1
test "$(httrack -O /dev/null -#2 "$1" | head -1)" == "$1 is of an unknown MIME type" || exit 1
}
mime '/a/b.html' 'text/html'

View File

@@ -8,7 +8,7 @@ set -euo pipefail
# relative path from <curr>'s directory to <link>
rel() {
local got
got=$(httrack -O /dev/null -#test=relative "$1" "$2")
got=$(httrack -O /dev/null -#l "$1" "$2")
test "$got" == "relative=$3" ||
{
echo "FAIL rel($1, $2): got '$got' want 'relative=$3'"
@@ -19,7 +19,7 @@ rel() {
# resolve <link> against origin <adr>/<fil> -> adr=.. fil=..
ident() {
local got
got=$(httrack -O /dev/null -#test=resolve "$1" "$2" "$3")
got=$(httrack -O /dev/null -#i "$1" "$2" "$3")
test "$got" == "$4" ||
{
echo "FAIL ident($1, $2, $3): got '$got' want '$4'"

View File

@@ -1,41 +0,0 @@
#!/bin/bash
#
set -euo pipefail
# Local save-name extension resolution (url_savename via -#test=savename <fil> <content-type>).
# Asserts on the basename of "savename: <path>".
name() {
out="$(httrack -O /dev/null -#test=savename "$1" "$2" | sed -n 's/^savename: //p')"
test "${out##*/}" == "$3" || {
echo "FAIL: '$1' '$2' -> '$out' (want '$3')"
exit 1
}
}
# #115: an unknown trailing ".token" is part of the name, keep it and append the type.
name '/article-1.884291' 'text/html' 'article-1.884291.html'
name '/news/story-12345.987654' 'text/html' 'story-12345.987654.html'
# Recognized extensions still collapse to the resolved type.
name '/page.php' 'text/html' 'page.html'
name '/page.asp' 'text/html' 'page.html'
name '/foo' 'text/html' 'foo.html'
# A bare trailing dot is not a tail to keep.
name '/page.' 'text/html' 'page.html'
# Soft-404 (#267/#408): a binary URL served as HTML is named .html.
name '/x.pdf' 'text/html' 'x.html'
name '/x.gif' 'text/html' 'x.html'
# Type agrees with the extension: keep it, no churn, no double extension.
name '/x.pdf' 'application/pdf' 'x.pdf'
name '/x.jpg' 'image/jpeg' 'x.jpg'
name '/x.html' 'text/html' 'x.html'
name '/x.js' 'application/x-javascript' 'x.js'
name '/types/data.json' 'application/json' 'data.json'
# Agreeing type must not rewrite the extension's casing (no strip-and-reappend).
name '/x.JPG' 'image/jpeg' 'x.JPG'

View File

@@ -1,17 +0,0 @@
#!/bin/bash
#
# The -#test dispatch itself: a bare -#test lists the registry, and an unknown
# name errors (non-zero, diagnostic) instead of silently passing.
set -eu
# Bare -#test lists known tests (printed to stderr).
list=$(httrack -#test 2>&1)
printf '%s\n' "$list" | grep -q "filter" || exit 1
printf '%s\n' "$list" | grep -q "cache-writefail" || exit 1
# Unknown name: non-zero exit + diagnostic, and no test result line.
rc=0
err=$(httrack -#test=bogus 2>&1) || rc=$?
test "$rc" -ne 0 || exit 1
printf '%s\n' "$err" | grep -q "Unknown self-test" || exit 1

View File

@@ -5,7 +5,7 @@ set -euo pipefail
# path simplify engine (fil_simplifie): collapses ./ and ../ segments.
simp() {
test "$(httrack -O /dev/null -#test=simplify "$1")" == "simplified=$2" || exit 1
test "$(httrack -O /dev/null -#1 "$1")" == "simplified=$2" || exit 1
}
simp './foo/bar/' 'foo/bar/'

View File

@@ -3,22 +3,23 @@
set -euo pipefail
# htssafe.h bounded string operations (driven by 'httrack -#test=strsafe').
# htssafe.h bounded string operations (driven by 'httrack -#8').
# Success path: every bounded op (strcpybuff/strcatbuff/strncatbuff/strlcpybuff)
# must behave correctly. 'run' selects the success path (vs the overflow modes).
# must behave correctly. Like the other -# debug modes, a trailing token is
# required (a bare '-#8' falls through to the usage screen).
rc=0
out=$(httrack -#test=strsafe run) || rc=$?
out=$(httrack -#8 run) || rc=$?
test "$rc" -eq 0 || exit 1
test "$out" == "strsafe: OK" || exit 1
# Overflow path: an over-capacity write into a sized buffer must be caught by
# the bounded macro and abort the process, not be silently truncated/completed.
# Assert the htssafe abort signature specifically, so the test cannot pass for
# an unrelated reason (e.g. the strsafe test being gone, which prints the
# registry to stderr and also exits non-zero).
# an unrelated reason (e.g. the -#8 mode being gone and falling through to the
# usage screen, which also exits non-zero).
# the bounded macro aborts (non-zero exit), so don't let set -e trip on it
err=$(httrack -#test=strsafe overflow "this string is far too long for the buffer" 2>&1) || true
err=$(httrack -#8 overflow "this string is far too long for the buffer" 2>&1) || true
case "$err" in
*"strsafe: NOT aborted"*)
echo "over-capacity write was NOT caught" >&2
@@ -35,7 +36,7 @@ esac
# capacity (4 bytes into a 4-byte buffer), so this also pins the boundary: a
# '<=' off-by-one in the capacity check would let it through (and print "NOT
# aborted"). Match the specific htsbuff abort message, not just any assert.
err=$(httrack -#test=strsafe overflow-buff "abcd" 2>&1) || true
err=$(httrack -#8 overflow-buff "abcd" 2>&1) || true
case "$err" in
*"strsafe: NOT aborted"*)
echo "htsbuff over-capacity write was NOT caught" >&2

View File

@@ -1,113 +0,0 @@
#!/bin/bash
# Issue #206: a continue/update crawl looped forever when the resume Range got a
# 416. Pass 1 leaves a partial + temp-ref; pass 2 must terminate and not loop.
set -u
: "${top_srcdir:=..}"
testdir=$(cd "$(dirname "$0")" && pwd)
server="${testdir}/local-server.py"
command -v python3 >/dev/null || ! echo "python3 not found; skipping" || exit 77
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/httrack_206.XXXXXX") || exit 1
serverpid=
crawlpid=
cleanup() {
test -n "$crawlpid" && kill -9 "$crawlpid" 2>/dev/null
if test -n "$serverpid"; then
kill "$serverpid" 2>/dev/null
wait "$serverpid" 2>/dev/null
fi
rm -rf "$tmpdir"
}
trap cleanup EXIT HUP INT QUIT PIPE TERM
# --- start the server, discover its ephemeral port --------------------------
# RESUME_COUNTER gets a byte per /resume/blob.txt request (pass-2 delta bounds re-gets).
serverlog="${tmpdir}/server.log"
counter="${tmpdir}/blobcount"
RESUME_COUNTER="$counter" python3 "$server" --root "${testdir}/server-root" >"$serverlog" 2>&1 &
serverpid=$!
port=
for _ in $(seq 1 50); do
line=$(head -n1 "$serverlog" 2>/dev/null)
if test "${line%% *}" == "PORT"; then
port="${line#PORT }"
break
fi
kill -0 "$serverpid" 2>/dev/null || {
echo "server exited early: $(cat "$serverlog")"
exit 1
}
sleep 0.1
done
test -n "$port" || {
echo "could not discover server port"
exit 1
}
base="http://127.0.0.1:${port}"
which httrack >/dev/null || {
echo "could not find httrack"
exit 1
}
out="${tmpdir}/crawl"
mkdir "$out"
common=(-O "$out" --quiet --disable-security-limits --robots=0 --timeout=30 --retries=0)
refdir="${out}/hts-cache/ref"
# --- pass 1: crawl, interrupt once the blob download is underway -------------
printf '[pass 1: interrupt mid-download] ..\t'
httrack "${common[@]}" "${base}/resume/index.html" >"${tmpdir}/log1" 2>&1 &
crawlpid=$!
# Wait until blob.txt is requested, then SIGTERM so httrack's exit handler
# finalizes the cache and serializes the temp-ref.
for _ in $(seq 1 300); do
test -s "$counter" && break
kill -0 "$crawlpid" 2>/dev/null || break
sleep 0.1
done
sleep 0.5
kill -TERM "$crawlpid" 2>/dev/null
wait "$crawlpid" 2>/dev/null
crawlpid=
test -n "$(find "$refdir" -name '*.ref' 2>/dev/null)" || {
echo "FAIL: no temp-ref survived pass 1; cannot drive #206"
exit 1
}
echo "OK (temp-ref present)"
before=$(wc -c <"$counter" 2>/dev/null || echo 0)
# --- pass 2: --continue -> resume Range -> 416, bounded against the #206 loop -
# Kill pass 2 after a deadline (portable stand-in for `timeout`, absent on macOS).
printf '[pass 2: resume must terminate] ..\t'
HANG_RC=137 # 128 + SIGKILL
httrack "${common[@]}" --continue "${base}/resume/index.html" >"${tmpdir}/log2" 2>&1 &
crawlpid=$!
(sleep 30 && kill -9 "$crawlpid" 2>/dev/null) &
guard=$!
rc=0
wait "$crawlpid" 2>/dev/null || rc=$?
crawlpid=
kill "$guard" 2>/dev/null || true
wait "$guard" 2>/dev/null || true
if test "$rc" -eq "$HANG_RC"; then
echo "FAIL: pass 2 did not terminate (#206 resume->416 loop)"
exit 1
fi
echo "OK (terminated, rc=$rc)"
# The fix re-gets once (resume Range + range-less re-get = 2): the lower bound
# rejects a drop-the-link non-fix (1), the upper bound rejects the loop (many).
after=$(wc -c <"$counter" 2>/dev/null || echo 0)
hits=$((after - before))
printf '[bounded re-get count] ..\t'
if test "$hits" -lt 2; then
echo "FAIL: only ${hits} pass-2 request(s); the stale partial was not re-got"
exit 1
fi
if test "$hits" -gt 8; then
echo "FAIL: ${hits} pass-2 requests for blob.txt (resume is looping)"
exit 1
fi
echo "OK (${hits} requests)"

View File

@@ -1,11 +0,0 @@
#!/bin/bash
#
# #157: a dotless, accented URL named .html on the first crawl must keep .html
# across an update -- not revert to the extensionless name.
: "${top_srcdir:=..}"
bash "$top_srcdir/tests/local-crawl.sh" --errors 0 --rerun \
--found 'intl/Instalação_CVS_no_Ubuntu.html' \
--not-found 'intl/Instalação_CVS_no_Ubuntu' \
httrack 'BASEURL/intl/index.html'

View File

@@ -1,17 +0,0 @@
#!/bin/bash
# Issues #32/#41: a Content-Length that disagrees with the body warns "bogus
# state (broken size)" and skips the cache; -%B (tolerant) accepts it.
: "${top_srcdir:=..}"
# Default: warn, but the file is still written.
bash "$top_srcdir/tests/local-crawl.sh" --errors 0 \
--found 'size/oversize.bin' \
--log-found 'bogus state \(broken size' \
httrack 'BASEURL/size/index.html'
# -%B (tolerant): no warning, file written.
bash "$top_srcdir/tests/local-crawl.sh" --errors 0 \
--found 'size/oversize.bin' \
--log-not-found 'bogus state' \
httrack 'BASEURL/size/index.html' '-%B'

View File

@@ -1,19 +0,0 @@
#!/bin/bash
# Issue #17: with "no error pages" (-o0), 4xx/5xx bodies must not be written;
# a genuine 0-byte 200 stays. Default (-o1) writes the error page. (#17's purge
# half also does not reproduce; the purge path is not exercised here.)
set -e
: "${top_srcdir:=..}"
# -o0: 404 suppressed, good page and the legit 0-byte 200 kept.
bash "$top_srcdir/tests/local-crawl.sh" --errors 1 \
--found 'errpage/good.html' \
--found 'errpage/empty.html' \
--not-found 'errpage/missing.html' \
httrack 'BASEURL/errpage/index.html' '-o0'
# Control -o1 (default): the 404 error page is written.
bash "$top_srcdir/tests/local-crawl.sh" --errors 1 \
--found 'errpage/missing.html' \
httrack 'BASEURL/errpage/index.html' '-o1'

View File

@@ -1,109 +0,0 @@
#!/bin/bash
# Issue #198: on a resumed download the server may answer the Range with a 206
# that starts *before* the offset we asked for (block-aligned ranges). httrack
# must honor the returned Content-Range, not blindly append, or the overlap
# bytes get duplicated and the file grows (corrupt PDFs). Pass 1 interrupts
# flaky.bin mid-body (partial + temp-ref); pass 2 resumes against a 206 that
# backs up 8 bytes. The result must equal the same bytes fetched whole (full.bin).
set -eu
: "${top_srcdir:=..}"
testdir=$(cd "$(dirname "$0")" && pwd)
server="${testdir}/local-server.py"
command -v python3 >/dev/null || ! echo "python3 not found; skipping" || exit 77
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/httrack_198.XXXXXX") || exit 1
serverpid=
crawlpid=
cleanup() {
if test -n "$crawlpid"; then kill -9 "$crawlpid" 2>/dev/null || true; fi
if test -n "$serverpid"; then
kill "$serverpid" 2>/dev/null || true
wait "$serverpid" 2>/dev/null || true
fi
rm -rf "$tmpdir"
}
trap cleanup EXIT HUP INT QUIT PIPE TERM
# OVERLAP_COUNTER gets a byte per flaky.bin request so pass 1 knows when to interrupt.
serverlog="${tmpdir}/server.log"
counter="${tmpdir}/hits"
resumed="${tmpdir}/resumed" # gets a byte when the server serves a resume 206
OVERLAP_COUNTER="$counter" OVERLAP_RESUMED="$resumed" \
python3 "$server" --root "${testdir}/server-root" \
>"$serverlog" 2>&1 &
serverpid=$!
port=
for _ in $(seq 1 50); do
line=$(head -n1 "$serverlog" 2>/dev/null)
if test "${line%% *}" == "PORT"; then
port="${line#PORT }"
break
fi
kill -0 "$serverpid" 2>/dev/null || {
echo "server exited early: $(cat "$serverlog")"
exit 1
}
sleep 0.1
done
test -n "$port" || {
echo "could not discover server port"
exit 1
}
base="http://127.0.0.1:${port}"
which httrack >/dev/null || {
echo "could not find httrack"
exit 1
}
out="${tmpdir}/crawl"
common=(-O "$out" --quiet --disable-security-limits --robots=0 --timeout=30 --retries=0 -c1)
refdir="${out}/hts-cache/ref"
# pass 1: interrupt once flaky.bin's prefix is streaming (partial + temp-ref).
printf '[pass 1: interrupt flaky.bin] ..\t'
httrack "${common[@]}" "${base}/overlap/index.html" >"${tmpdir}/log1" 2>&1 &
crawlpid=$!
for _ in $(seq 1 300); do
test -s "$counter" && break
kill -0 "$crawlpid" 2>/dev/null || break
sleep 0.1
done
sleep 0.5
kill -TERM "$crawlpid" 2>/dev/null || true
wait "$crawlpid" 2>/dev/null || true
crawlpid=
test -n "$(find "$refdir" -name '*.ref' 2>/dev/null)" || {
echo "FAIL: no temp-ref survived pass 1; cannot drive the resume"
exit 1
}
echo "OK (temp-ref present)"
# pass 2: --continue -> resume Range -> 206 that starts 8 bytes early.
printf '[pass 2: resume flaky.bin] ..\t'
httrack "${common[@]}" --continue "${base}/overlap/index.html" >"${tmpdir}/log2" 2>&1 || true
echo "OK"
# Guard against a silent full re-download: the byte-compare below only tests the
# fix if pass 2 actually went through the resume Range -> 206 path.
printf '[resume path was exercised] ..\t'
if ! test -s "$resumed"; then
echo "FAIL: pass 2 never triggered a resume 206; the overlap fix was not exercised"
exit 1
fi
echo "OK"
printf '[resumed file is not corrupted] ..\t'
dir=$(find "$out" -maxdepth 1 -type d -name '127.0.0.1*' | head -1)
flaky="${dir}/overlap/flaky.bin"
full="${dir}/overlap/full.bin"
if ! test -f "$flaky" || ! test -f "$full"; then
echo "FAIL: flaky.bin or full.bin missing after pass 2"
exit 1
fi
if ! cmp -s "$flaky" "$full"; then
echo "FAIL: resumed flaky.bin ($(wc -c <"$flaky")) != full.bin ($(wc -c <"$full")); overlap duplicated"
exit 1
fi
echo "OK ($(wc -c <"$flaky") bytes, byte-identical)"

View File

@@ -1,16 +0,0 @@
#!/bin/bash
#
# A -mime: exclusion must abort the transfer on the response Content-Type, not
# fetch the whole 1 MB body then discard it (#58). The bytes-received guard is
# the real one: the file is absent either way, but only the fix keeps the count
# tiny (header only) instead of pulling the body. Match it positively (a small,
# <=4-digit count) so a vanished/reworded summary line fails rather than passes.
: "${top_srcdir:=..}"
bash "$top_srcdir/tests/local-crawl.sh" --errors 0 \
--found 'mimex/real.html' \
--not-found 'mimex/blob.pdf' \
--log-found 'excluded by MIME type filter' \
--log-found '\[[0-9]{1,4} bytes received' \
httrack 'BASEURL/mimex/index.html' '-mime:application/pdf'

View File

@@ -26,7 +26,6 @@ TESTS = \
00_runnable.test \
01_engine-cache.test \
01_engine-cache-golden.test \
01_engine-cache-writefail.test \
01_engine-charset.test \
01_engine-cmdline.test \
01_engine-cookies.test \
@@ -34,7 +33,6 @@ TESTS = \
01_engine-dns.test \
01_engine-doitlog.test \
01_engine-entities.test \
01_engine-filelist.test \
01_engine-filter.test \
01_engine-hashtable.test \
01_engine-idna.test \
@@ -42,8 +40,6 @@ TESTS = \
01_engine-parse.test \
01_engine-rcfile.test \
01_engine-relative.test \
01_engine-savename.test \
01_engine-selftest-dispatch.test \
01_engine-simplify.test \
01_engine-strsafe.test \
02_manpage-regen.test \
@@ -62,12 +58,6 @@ TESTS = \
16_local-assume.test \
17_local-empty-ct.test \
18_local-update.test \
19_local-connect-fallback.test \
20_local-resume-loop.test \
21_local-intl-update.test \
22_local-broken-size.test \
23_local-errpage.test \
24_local-resume-overlap.test \
25_local-mime-exclude.test
19_local-connect-fallback.test
CLEANFILES = check-network_sh.cache

View File

@@ -14,9 +14,7 @@
# Usage:
# bash local-crawl.sh [--tls] [--root DIR] \
# --errors N --files N --found PATH ... --directory PATH ... \
# --log-found REGEX ... --log-not-found REGEX ... \
# httrack BASEURL/some/path [httrack-args...]
# --log-found/--log-not-found grep (ERE) the crawl's hts-log.txt.
set -u
@@ -109,7 +107,7 @@ while test "$pos" -lt "$nargs"; do
audit+=("${args[$pos]}" "${args[$((pos + 1))]}")
pos=$((pos + 1))
;;
--found | --not-found | --directory | --log-found | --log-not-found)
--found | --not-found | --directory)
audit+=("${args[$pos]}" "${args[$((pos + 1))]}")
pos=$((pos + 1))
;;
@@ -198,15 +196,6 @@ if test -n "$rerun"; then
exit 1
}
result "OK (update)"
# The update summary reports "files updated"; a fresh crawl never does. Assert
# it so a regression that bypasses the cache (re-crawls fresh) can't pass.
info "checking update used the cache"
if grep -aqE "mirror complete in .*files updated" "${out}/hts-log.txt"; then
result "OK"
else
result "update pass did not report cache activity"
exit 1
fi
fi
# --- discover the single host root (127.0.0.1_<port> or 127.0.0.1) -----------
@@ -259,22 +248,6 @@ while test "$i" -lt "${#audit[@]}"; do
exit 1
fi
;;
--log-found)
i=$((i + 1))
info "checking log matches ${audit[$i]}"
if grep -aqE "${audit[$i]}" "${out}/hts-log.txt"; then result "OK"; else
result "not in log"
exit 1
fi
;;
--log-not-found)
i=$((i + 1))
info "checking log lacks ${audit[$i]}"
if grep -aqE "${audit[$i]}" "${out}/hts-log.txt"; then
result "present in log"
exit 1
else result "OK"; fi
;;
esac
i=$((i + 1))
done

View File

@@ -15,7 +15,6 @@ stdlib only (http.server + ssl) -- no new build or runtime dependency.
import argparse
import os
import time
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import quote, unquote, urlsplit
@@ -177,170 +176,6 @@ class Handler(SimpleHTTPRequestHandler):
body, ctype = self.TYPE_MATRIX[path]
self.send_raw(body, ctype)
# --- MIME-type exclusion abort (issue #58) -----------------------------
# A -mime:application/pdf filter must abort the transfer once the header
# arrives, not download the whole body and discard it.
def route_mimex_index(self):
self.send_html(
'\t<a href="blob.pdf">pdf</a>\n' '\t<a href="real.html">real</a>\n'
)
# 1 MB body: the fix aborts after the header, so httrack's "bytes received"
# stays tiny; without it the engine reads the body and the count jumps.
MIMEX_BLOB = b"%PDF-1.4\n" + b"\x00" * (1024 * 1024)
def route_mimex_blob(self):
self.send_raw(self.MIMEX_BLOB, "application/pdf")
def route_mimex_real(self):
self.send_raw(b"<html><body>real</body></html>", "text/html")
# --- special chars in URLs across an update (issue #157) ---------------
# A dotless, accented basename served as text/html (MediaWiki style). The
# name the first crawl picks (.html) must survive the update pass.
INTL_NAME = "Instalação_CVS_no_Ubuntu"
def route_intl_index(self):
self.send_html('\t<a href="%s">accented</a>\n' % self.INTL_NAME)
def route_intl_page(self):
self.send_raw(b"<html><body>accented page</body></html>\n", "text/html")
# 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
RESUME_LEN = len(RESUME_PREFIX) + 4096 # declared length never delivered
_resume_started = False
def route_resume_index(self):
self.send_html('\t<a href="blob.txt">blob</a>')
def route_resume(self):
counter = os.environ.get("RESUME_COUNTER")
if counter:
with open(counter, "a") as fp:
fp.write("x")
# First GET: stall mid-body so the crawl can be interrupted with a partial.
if not Handler._resume_started:
Handler._resume_started = True
self.send_response(200)
self.send_header("Content-Type", "image/png")
self.send_header("Content-Length", str(self.RESUME_LEN))
self.send_header("Accept-Ranges", "bytes")
self.end_headers()
if self.command != "HEAD":
self.wfile.write(self.RESUME_PREFIX)
self.wfile.flush()
try:
while True:
time.sleep(3600)
except OSError:
pass
return
self.send_response(416, "Requested Range Not Satisfiable")
self.send_header("Content-Type", "image/png")
self.send_header("Content-Range", "bytes */%d" % self.RESUME_LEN)
self.send_header("Content-Length", "0")
self.end_headers()
# 206 resume must honor the server's Content-Range, not the offset we asked
# for (#198): a server resuming a few bytes *before* the request must not
# leave httrack duplicating the overlap onto the partial. flaky.bin
# interrupts once then resumes OVERLAP_EARLY bytes early; full.bin serves
# the identical bytes in one shot, so the test can compare the two.
OVERLAP_BLOB = b"%PDF-1.4\n" + bytes((i * 37 + 11) % 256 for i in range(8000))
OVERLAP_EARLY = 8
OVERLAP_PREFIX_LEN = 4000 # flushed before the stall
_overlap_started = False
def route_overlap_index(self):
self.send_html('\t<a href="flaky.bin">flaky</a>\n\t<a href="full.bin">full</a>')
def route_overlap_full(self):
self.send_raw(self.OVERLAP_BLOB, "application/octet-stream")
def route_overlap(self):
counter = os.environ.get("OVERLAP_COUNTER")
if counter:
with open(counter, "a") as fp:
fp.write("x")
blob = self.OVERLAP_BLOB
rng = self.headers.get("Range")
# First GET: stream a prefix then stall, so the crawl can be interrupted
# mid-body (partial + temp-ref on disk).
if rng is None and not Handler._overlap_started:
Handler._overlap_started = True
self.send_response(200)
self.send_header("Content-Type", "application/octet-stream")
self.send_header("Content-Length", str(len(blob)))
self.send_header("Accept-Ranges", "bytes")
self.end_headers()
if self.command != "HEAD":
self.wfile.write(blob[: self.OVERLAP_PREFIX_LEN])
self.wfile.flush()
try:
while True:
time.sleep(3600)
except OSError:
pass
return
if rng is None: # no resume request: serve the whole file
return self.route_overlap_full()
# Resume: honor the Range, but back up OVERLAP_EARLY bytes.
start = (
int(rng[len("bytes=") :].split("-")[0]) if rng.startswith("bytes=") else 0
)
start = max(0, start - self.OVERLAP_EARLY)
# Signal that the resume Range -> 206 path actually fired, so the test
# can prove it was exercised (not a silent full re-download).
resumed = os.environ.get("OVERLAP_RESUMED")
if resumed:
with open(resumed, "a") as fp:
fp.write("x")
part = blob[start:]
self.send_response(206, "Partial Content")
self.send_header("Content-Type", "application/octet-stream")
self.send_header("Content-Length", str(len(part)))
self.send_header(
"Content-Range", "bytes %d-%d/%d" % (start, len(blob) - 1, len(blob))
)
self.end_headers()
if self.command != "HEAD":
self.wfile.write(part)
# error pages / 0-byte files (#17): -o0 ("no error pages") must keep 4xx/5xx
# bodies off disk; a genuine 0-byte 200 is a valid file and stays.
def route_errpage_index(self):
self.send_html(
'\t<a href="good.html">good</a>\n'
'\t<a href="missing.html">missing</a>\n'
'\t<a href="empty.html">empty</a>\n'
)
def route_errpage_good(self):
self.send_raw(b"<html><body>good page</body></html>\n", "text/html")
def route_errpage_missing(self):
self.send_html("\t404 error body", status=404, extra_status="Not Found")
def route_errpage_empty(self):
self.send_raw(b"", "text/html")
# broken Content-Length (#32/#41): declared size != bytes sent. httrack
# warns "bogus state (broken size)" and skips the cache unless -%B.
def route_size_index(self):
self.send_html('\t<a href="oversize.bin">over</a>\n')
def route_size_oversize(self):
body = b"A" * 100
self.send_response(200)
self.send_header("Content-Type", "application/octet-stream")
self.send_header("Content-Length", str(len(body) - 2)) # lie: too short
self.send_header("Connection", "close")
self.end_headers()
if self.command != "HEAD":
self.wfile.write(body)
ROUTES = {
"/cookies/entrance.php": route_entrance,
"/cookies/second.php": route_second,
@@ -360,22 +195,6 @@ class Handler(SimpleHTTPRequestHandler):
"/types/style.css": route_types,
"/types/data.json": route_types,
"/types/gen.php": route_types,
"/intl/index.html": route_intl_index,
"/intl/" + INTL_NAME: route_intl_page,
"/resume/index.html": route_resume_index,
"/resume/blob.txt": route_resume,
"/overlap/index.html": route_overlap_index,
"/overlap/flaky.bin": route_overlap,
"/overlap/full.bin": route_overlap_full,
"/size/index.html": route_size_index,
"/size/oversize.bin": route_size_oversize,
"/errpage/index.html": route_errpage_index,
"/errpage/good.html": route_errpage_good,
"/errpage/missing.html": route_errpage_missing,
"/errpage/empty.html": route_errpage_empty,
"/mimex/index.html": route_mimex_index,
"/mimex/blob.pdf": route_mimex_blob,
"/mimex/real.html": route_mimex_real,
}
# --- dispatch ----------------------------------------------------------
@@ -383,8 +202,7 @@ class Handler(SimpleHTTPRequestHandler):
def dispatch(self):
self._set_cookies = []
path = urlsplit(self.path).path
# Match percent-encoded paths (accented #157 route) by their decoded form.
handler = self.ROUTES.get(path) or self.ROUTES.get(unquote(path))
handler = self.ROUTES.get(path)
if handler is not None:
handler(self)
return True