Compare commits

..

4 Commits

Author SHA1 Message Date
Xavier Roche
2ffe8d5582 tests: assert the retry exhaustion, not the per-platform failure message
The message a cut connection produces depends on whether any bytes arrived, so
matching it would fail on a runner that sees none.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-27 09:25:38 +02:00
Xavier Roche
6457828b72 tests: make the re-fetch assertions catch a crawl that never re-fetched
The new test passed with no second pass at all, so a regression that stopped
re-fetching would have looked green. Assert the failure the fixture provokes,
give stay.bin a fresh pass-2 body so a fix that stopped overwriting anything
fails, and compare reset.bin by checksum rather than by length.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-27 09:18:27 +02:00
Xavier Roche
f118cd86d3 tests: assert the failed re-fetch keeps its bytes on every platform
Test 93 filtered reset.bin out of its bucket lists because a connection killed
before the status line surfaced differently per platform. It no longer does, so
assert the resource like any other: unchanged, with the bytes pass 1 mirrored.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-27 09:05:56 +02:00
Xavier Roche
1373530f17 A failed re-fetch overwrote the mirrored file with the aborted read's debris
A transfer that dies before a complete response has no body, yet the save path
still consulted r.adr, which at that point holds whatever the aborted header
read left behind: raw status-line bytes, or an empty buffer that truncated the
file to zero on macOS. Require a successful transfer, as the empty-body half of
the condition already did.

Closes #748

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-27 08:59:43 +02:00
23 changed files with 112 additions and 636 deletions

View File

@@ -585,6 +585,14 @@ static int create_back_tmpfile(httrackp *opt, lien_back *const back,
return 0;
}
/* Move src onto dst; RENAME does not clobber an existing target on Windows. */
static hts_boolean replace_file(const char *src, const char *dst) {
if (RENAME(src, dst) == 0)
return HTS_TRUE;
(void) UNLINK(dst);
return RENAME(src, dst) == 0 ? HTS_TRUE : HTS_FALSE;
}
/* Commit or restore a re-fetch backup (#77 follow-up): a re-fetch over an
existing file moved the good copy to back->tmpfile before truncating url_sav.
commit keeps the new file and drops the backup; else restore it so an aborted
@@ -602,7 +610,7 @@ static void back_finalize_backup(httrackp *opt, lien_back *const back,
}
/* On failure keep the backup: an orphaned temp beats losing the good copy.
*/
if (!hts_rename_over(back->tmpfile, back->url_sav))
if (!replace_file(back->tmpfile, back->url_sav))
hts_log_print(opt, LOG_WARNING | LOG_ERRNO,
"could not restore %s; previous copy kept as %s",
back->url_sav, back->tmpfile);
@@ -733,7 +741,7 @@ int back_finalize(httrackp * opt, cache_back * cache, struct_back * sback,
"Read error when decompressing");
}
UNLINK(unpacked);
} else if (hts_rename_over(unpacked, back[p].url_sav)) {
} else if (replace_file(unpacked, back[p].url_sav)) {
/* The temp bypassed filecreate(), which is what chmods. */
#ifndef _WIN32
chmod(back[p].url_sav, HTS_ACCESS_FILE);

View File

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

View File

@@ -2692,11 +2692,7 @@ HTSEXT_API int structcheck(const char *path) {
if (!S_ISDIR(st.st_mode)) {
#if HTS_REMOVE_ANNOYING_INDEX
if (S_ISREG(st.st_mode)) { /* Regular file in place ; move it and create directory */
/* bounded here, not by the path-length guard far above */
if (!sprintfbuff(tmpbuf, "%s.txt", file)) {
errno = ENAMETOOLONG;
return -1;
}
sprintf(tmpbuf, "%s.txt", file);
if (rename(file, tmpbuf) != 0) { /* Can't rename regular file */
return -1;
}
@@ -2804,11 +2800,7 @@ HTSEXT_API int structcheck_utf8(const char *path) {
if (!S_ISDIR(st.st_mode)) {
#if HTS_REMOVE_ANNOYING_INDEX
if (S_ISREG(st.st_mode)) { /* Regular file in place ; move it and create directory */
/* bounded here, not by the path-length guard far above */
if (!sprintfbuff(tmpbuf, "%s.txt", file)) {
errno = ENAMETOOLONG;
return -1;
}
sprintf(tmpbuf, "%s.txt", file);
if (RENAME(file, tmpbuf) != 0) { /* Can't rename regular file */
return -1;
}
@@ -3828,12 +3820,11 @@ int htsAddLink(htsmoduleStruct * str, char *link) {
strcpybuff(codebase, heap(ptr)->fil);
else
strcpybuff(codebase, heap(heap(ptr)->precedent)->fil);
// empty codebase has no last char; codebase-1 would underflow
a = codebase[0] != '\0' ? codebase + strlen(codebase) - 1 : codebase;
a = codebase + strlen(codebase) - 1;
while((*a) && (*a != '/') && (a > codebase))
a--;
if (*a == '/')
*(a + 1) = '\0'; // cut
*(a + 1) = '\0'; // couper
} else { // couper http:// éventuel
if (strfield(codebase, "http://")) {
char BIGSTK tempo[HTS_URLMAXSIZE * 2];

View File

@@ -1131,10 +1131,12 @@ int http_sendhead(httrackp * opt, t_cookie * cookie, int mode,
// Compression accepted ?
if (retour->req.http11) {
hts_boolean compressible =
(!retour->req.range_used && !retour->req.nocompression);
hts_boolean compressible = HTS_FALSE;
hts_boolean secure = HTS_FALSE;
#if HTS_USEZLIB
compressible = (!retour->req.range_used && !retour->req.nocompression);
#endif
#if HTS_USEOPENSSL
secure = retour->ssl ? HTS_TRUE : HTS_FALSE;
#endif

View File

@@ -43,7 +43,9 @@ Please visit our Website: http://www.httrack.com
#include "htsencoding.h"
#include "htssniff.h"
#include "htscodec.h"
#if HTS_USEZLIB
#include "htszlib.h"
#endif
#include <ctype.h>
#include <limits.h>

View File

@@ -43,7 +43,6 @@ Please visit our Website: http://www.httrack.com
#include "htsglobal.h"
#include "htscore.h"
#include "htsmodules.h"
#include "htsback.h"
#include "htsdefines.h"
#include "htslib.h"
@@ -64,7 +63,9 @@ Please visit our Website: http://www.httrack.com
#include "htswarc.h"
#include "htschanges.h"
#include "htssinglefile.h"
#if HTS_USEZLIB
#include "htszlib.h"
#endif
#if HTS_USEZSTD
#include <zstd.h>
#endif
@@ -2622,68 +2623,6 @@ static int st_savename(httrackp *opt, int argc, char **argv) {
return 0;
}
/* an empty fil started htsAddLink's codebase walk before the buffer (#730) */
static int st_addlink(httrackp *opt, int argc, char **argv) {
htsmoduleStruct BIGSTK str;
cache_back cache;
struct_back *sback;
hash_struct hash;
int ptr = 0;
int i;
(void) argc;
(void) argv;
memset(&cache, 0, sizeof(cache));
cache.hashtable = (void *) coucal_new(0);
sback = back_new(opt, opt->maxsoc * 32 + 1024);
/* same wiring as hts_mirror (htscore.c) */
hash_init(opt, &hash, opt->urlhack);
hash.liens = (const lien_url *const *const *) &opt->liens;
opt->hash = &hash;
hts_record_init(opt);
memset(&str, 0, sizeof(str));
str.opt = opt;
str.sback = sback;
str.cache = &cache;
str.hashptr = &hash;
str.ptr_ = &ptr;
str.addLink = htsAddLink;
/* [0] is the underflow; [1] and [2] are controls that the trim is unchanged.
A query-only link is the one that notices the trim at all: for the others
ident_url_relatif() re-derives the directory from the path it is given. */
for (i = 0; i < 3; i++) {
static const char *const fil[3] = {"", "/dir/page.html", "/dir/page.html"};
static const char *const lnk[3] = {"sub/page.html", "sub/page.html",
"?x=1"};
static const char *const want[3] = {
"untouched", "http://www.example.com/dir/sub/page.html",
"http://www.example.com/dir/?x=1"};
char BIGSTK loc[HTS_URLMAXSIZE * 2];
char BIGSTK link[HTS_URLMAXSIZE];
strcpybuff(loc, "untouched");
strcpybuff(link, lnk[i]);
str.localLink = loc;
str.localLinkSize = (int) sizeof(loc);
if (!hts_record_link(opt, "www.example.com", fil[i], "", "", "", ""))
return 1;
ptr = heap_top_index();
str.url_host = heap(ptr)->adr;
str.url_file = heap(ptr)->fil;
assertf(htsAddLink(&str, link) == 0); /* refused by the wizard either way */
if (strcmp(loc, want[i]) != 0) {
fprintf(stderr, "addlink[%d]: got '%s' want '%s'\n", i, loc, want[i]);
return 1;
}
}
printf("addlink self-test OK\n");
return 0;
}
static int st_cache(httrackp *opt, int argc, char **argv) {
int err;
@@ -3325,81 +3264,6 @@ static int st_topindex(httrackp *opt, int argc, char **argv) {
return 0;
}
/* Build a path of exactly len chars under base; returns that length. */
static size_t st_structcheck_longpath(char *dst, size_t dstsize,
const char *base, size_t len) {
size_t n = strlen(base);
assertf(len < dstsize && n + 2 <= len);
memmove(dst, base, n);
while (n < len) {
size_t seg = len - n - 1;
if (seg > 200) /* stay under the usual 255-byte component limit */
seg = len - n == 202 ? 199 : 200; /* never leave a bare separator */
dst[n++] = '/';
memset(dst + n, 'x', seg);
n += seg;
}
dst[n] = '\0';
return n;
}
/* The path guard, and the <name>.txt rename structcheck() performs when a
regular file sits where a directory has to go (#745). */
static int st_structcheck(httrackp *opt, int argc, char **argv) {
char BIGSTK path[HTS_URLMAXSIZE * 2];
char BIGSTK target[HTS_URLMAXSIZE * 2];
FILE *fp;
(void) opt;
if (argc < 1) {
fprintf(stderr, "usage: -#test=structcheck <writable directory>\n");
return 1;
}
/* over the guard: refused before a single directory is created */
st_structcheck_longpath(path, sizeof(path), argv[0], HTS_URLMAXSIZE + 1);
errno = 0;
assertf(structcheck(path) == -1);
assertf(errno == EINVAL);
errno = 0;
assertf(structcheck_utf8(path) == -1);
assertf(errno == EINVAL);
{
char *const sep = strchr(path + strlen(argv[0]) + 1, '/');
assertf(sep != NULL);
sep[1] = '\0'; /* the outermost component it would have created */
assertf(!dir_exists(path));
}
/* a regular file where a directory belongs is renamed to <name>.txt */
snprintf(path, sizeof(path), "%s/sc", argv[0]);
fp = fopen(path, "wb");
assertf(fp != NULL);
fclose(fp);
snprintf(path, sizeof(path), "%s/sc/sub/", argv[0]);
assertf(structcheck(path) == 0);
assertf(dir_exists(path));
snprintf(target, sizeof(target), "%s/sc.txt", argv[0]);
assertf(fexist(target));
/* the utf-8 entry point carries the same rename */
snprintf(path, sizeof(path), "%s/u8", argv[0]);
fp = FOPEN(path, "wb");
assertf(fp != NULL);
fclose(fp);
snprintf(path, sizeof(path), "%s/u8/sub/", argv[0]);
assertf(structcheck_utf8(path) == 0);
assertf(dir_exists(path));
snprintf(target, sizeof(target), "%s/u8.txt", argv[0]);
assertf(fexist_utf8(target));
printf("structcheck self-test OK\n");
return 0;
}
/* Each inplace_escape_*() must equal escape_*() on a copy. */
static int st_inplace_escape(httrackp *opt, int argc, char **argv) {
/* >255 bytes forces the helper's malloct path, not the stack buffer */
@@ -3513,6 +3377,7 @@ static int st_status(httrackp *opt, int argc, char **argv) {
return 0;
}
#if HTS_USEZLIB
/* Deflate src->path at windowBits (16+ gzip, + zlib, - raw); 0 on success. */
static int ae_write_packed(const char *path, int windowBits,
const unsigned char *src, size_t len) {
@@ -3586,6 +3451,7 @@ static int ae_write_collision(const char *path, const unsigned char *src,
freet(buf);
return ok ? 0 : 1;
}
#endif
/* Write src[0..len) to path as-is; 0 on success. */
static int ae_write_raw(const char *path, const unsigned char *src,
@@ -3635,6 +3501,7 @@ static int st_acceptencoding(httrackp *opt, int argc, char **argv) {
assertf(strstr(on, "br") == NULL && strstr(on, "zstd") == NULL);
assertf((strstr(tls, ", br") != NULL) == (HTS_USEBROTLI != 0));
assertf((strstr(tls, "zstd") != NULL) == (HTS_USEZSTD != 0));
#if HTS_USEZLIB
if (argc >= 1) {
static const int windowBits[] = {16 + MAX_WBITS, MAX_WBITS, -MAX_WBITS};
const unsigned char small[] =
@@ -3713,6 +3580,10 @@ static int st_acceptencoding(httrackp *opt, int argc, char **argv) {
}
freet(body);
}
#else
(void) argc;
(void) argv;
#endif
printf("acceptencoding self-test OK: %s\n", on);
return 0;
}
@@ -4057,6 +3928,7 @@ static int st_sitemap(httrackp *opt, int argc, char **argv) {
freet(big);
}
#if HTS_USEZLIB
/* A highly compressible document decodes without running away: the ratio
budget cannot bind (deflate tops out near 1032:1), so this pins the
decompression path itself rather than the 64 MiB ceiling. */
@@ -4107,11 +3979,13 @@ static int st_sitemap(httrackp *opt, int argc, char **argv) {
freet(z);
freet(x);
}
#endif
/* An unterminated <loc> at end of buffer must not read past it. */
assertf(sm_scan("<urlset><loc>http://h.test/a", 100, &idx, &c) == 0);
assertf(sm_scan("<urlset><lo", 100, &idx, &c) == 0);
#if HTS_USEZLIB
/* A gzip-framed document is decompressed before scanning. */
{
const char *const xml =
@@ -4141,6 +4015,7 @@ static int st_sitemap(httrackp *opt, int argc, char **argv) {
assertf(hts_sitemap_scan(z, 4, 100, &idx, sm_take, &c) == -1);
freet(z);
}
#endif
/* robots.txt: only Sitemap: records, comments stripped, case-insensitive,
and group-independent (no User-agent line needed). */
@@ -6214,116 +6089,6 @@ static int st_changes(httrackp *opt, int argc, char **argv) {
return err;
}
/* #747: a thread is outstanding from the moment hts_newthread() returns, not
from the moment it starts running, or a wait right after the spawn joins
nothing. One thread per round is what makes the old bug visible: the wait
had to find the counter at zero, and a batch of spawns gives the earlier
threads time to raise it. Unfixed, one round in two caught it, so the round
count is what turns that into a reliable failure. */
#define THREADWAIT_N 8
#define THREADWAIT_ROUNDS 16
#define THREADWAIT_SLEEP_MS 50
#define THREADWAIT_GATE_MS 10000
static htsmutex threadwait_lock = HTSMUTEX_INIT;
static int threadwait_done = 0;
static hts_boolean threadwait_gated = HTS_FALSE;
static int threadwait_count(void) {
int n;
hts_mutexlock(&threadwait_lock);
n = threadwait_done;
hts_mutexrelease(&threadwait_lock);
return n;
}
static void threadwait_thread(void *arg) {
(void) arg;
Sleep(THREADWAIT_SLEEP_MS);
hts_mutexlock(&threadwait_lock);
threadwait_done++;
hts_mutexrelease(&threadwait_lock);
}
/* Stays outstanding until the gate clears, so wait_n() can be asked to leave a
known number of live threads behind. Bounded: a wait_n() that wrongly drains
them would otherwise never return, and hang the suite instead of failing. */
static void threadwait_gated_thread(void *arg) {
int waited;
(void) arg;
for (waited = 0; waited < THREADWAIT_GATE_MS; waited += 10) {
hts_boolean gated;
hts_mutexlock(&threadwait_lock);
gated = threadwait_gated;
hts_mutexrelease(&threadwait_lock);
if (!gated)
break;
Sleep(10);
}
hts_mutexlock(&threadwait_lock);
threadwait_done++;
hts_mutexrelease(&threadwait_lock);
}
static int st_threadwait(httrackp *opt, int argc, char **argv) {
int err = 0;
int i, round;
(void) opt;
(void) argc;
(void) argv;
/* htsthread_wait() joins a thread spawned just before it */
for (round = 0; round < THREADWAIT_ROUNDS && !err; round++) {
hts_mutexlock(&threadwait_lock);
threadwait_done = 0;
hts_mutexrelease(&threadwait_lock);
if (hts_newthread(threadwait_thread, NULL) != 0) {
fprintf(stderr, "threadwait: cannot spawn\n");
return 1;
}
htsthread_wait();
if (threadwait_count() != 1) {
fprintf(stderr, "threadwait: round %d returned before the thread ran\n",
round);
err = 1;
}
}
/* htsthread_wait_n(n) leaves n behind rather than draining everything */
hts_mutexlock(&threadwait_lock);
threadwait_done = 0;
threadwait_gated = HTS_TRUE;
hts_mutexrelease(&threadwait_lock);
for (i = 0; i < THREADWAIT_N; i++) {
if (hts_newthread(threadwait_gated_thread, NULL) != 0) {
fprintf(stderr, "threadwait: cannot spawn a gated thread\n");
return 1;
}
}
htsthread_wait_n(THREADWAIT_N);
if (threadwait_count() != 0) {
fprintf(stderr, "threadwait: wait_n(%d) joined %d gated threads\n",
THREADWAIT_N, threadwait_count());
err = 1;
}
hts_mutexlock(&threadwait_lock);
threadwait_gated = HTS_FALSE;
hts_mutexrelease(&threadwait_lock);
htsthread_wait();
if (threadwait_count() != THREADWAIT_N) {
fprintf(stderr, "threadwait: wait left %d/%d gated threads running\n",
THREADWAIT_N - threadwait_count(), THREADWAIT_N);
err = 1;
}
printf("threadwait self-test: %s\n", err ? "FAIL" : "OK");
return err;
}
#define CHANGES_RACE_FILES 8
#define CHANGES_RACE_ROUNDS 400
@@ -6338,8 +6103,11 @@ static void changes_race_notify(httrackp *opt, int n) {
hts_changes_notify(opt, "race.example", fil, save, HTS_TRUE, HTS_FALSE);
}
/* htsthread_wait() counts a thread only once it is running, so it can return
before any of them started; join on our own counter instead. */
static htsmutex changes_race_lock = HTSMUTEX_INIT;
static int changes_race_started = 0;
static int changes_race_live = 0;
static int changes_race_count(int *which) {
int n;
@@ -6359,6 +6127,9 @@ static void changes_race_thread(void *arg) {
hts_mutexrelease(&changes_race_lock);
for (i = 0; i < CHANGES_RACE_ROUNDS; i++)
changes_race_notify(opt, i % CHANGES_RACE_FILES);
hts_mutexlock(&changes_race_lock);
changes_race_live--;
hts_mutexrelease(&changes_race_lock);
}
/* A transfer thread the crawl never joins (FTP) reaches hts_changes_notify()
@@ -6413,6 +6184,7 @@ static int st_changes_race(httrackp *opt, int argc, char **argv) {
threads reaching a fresh one together race on the init itself. */
hts_mutexlock(&changes_race_lock);
changes_race_started = 0;
changes_race_live = 4;
hts_mutexrelease(&changes_race_lock);
for (i = 0; i < 4; i++) {
if (hts_newthread(changes_race_thread, opt) != 0) {
@@ -6426,7 +6198,8 @@ static int st_changes_race(httrackp *opt, int argc, char **argv) {
for (i = 0; i < 64; i++)
hts_changes_report(opt, &out);
hts_changes_close_opt(opt);
htsthread_wait();
while (changes_race_count(&changes_race_live) > 0)
Sleep(10);
/* Sealed: a straggler must be dropped, not start a report nobody writes. */
changes_race_notify(opt, CHANGES_RACE_FILES + 1);
@@ -6504,8 +6277,6 @@ static const struct selftest_entry {
st_changes},
{"changes-race", "<dir>", "--changes under a late transfer thread (#714)",
st_changes_race},
{"threadwait", "", "htsthread_wait() joins threads spawned just before it",
st_threadwait},
{"pause", "", "randomized inter-file pause target self-test", st_pause},
{"relative", "<link> <curr-file>", "relative link between two paths",
st_relative},
@@ -6536,8 +6307,6 @@ static const struct selftest_entry {
{"fsize", "<dir>", "file size past the 2GB signed-32-bit wrap", st_fsize},
{"growsize", "", "buffer capacity for a 64-bit file size (no int wrap)",
st_growsize},
{"addlink", "", "htsAddLink codebase walk over an empty current path",
st_addlink},
{"cache", "<dir>", "cache read/write round-trip self-test", st_cache},
{"cacheindex", "", "cache-index (.ndx) parse must stay in bounds",
st_cacheindex},
@@ -6561,9 +6330,6 @@ static const struct selftest_entry {
{"useragent", "", "default User-Agent self-test", st_useragent},
{"makeindex", "[dir]", "hts_finish_makeindex footer/refresh self-test",
st_makeindex},
{"structcheck", "<dir>",
"structcheck path guard and the <name>.txt rename it performs",
st_structcheck},
{"topindex", "[dir]",
"hts_buildtopindex charset handling of a non-ASCII project dir",
st_topindex},

View File

@@ -1108,8 +1108,13 @@ hts_boolean singlefile_rewrite_file(httrackp *opt, const char *root,
(void) chmod(fconv(catbuff, sizeof(catbuff), StringBuff(tmp)),
HTS_ACCESS_FILE);
#endif
if (ok)
ok = hts_rename_over(StringBuff(tmp), page_path);
if (ok) {
/* RENAME does not clobber an existing target on Windows. */
if (RENAME(StringBuff(tmp), page_path) != 0) {
(void) UNLINK(page_path);
ok = RENAME(StringBuff(tmp), page_path) == 0 ? HTS_TRUE : HTS_FALSE;
}
}
if (!ok) {
hts_log_print(opt, LOG_ERROR, "single-file: could not rewrite %s",
page_path);

View File

@@ -1443,15 +1443,3 @@ HTSEXT_API hts_boolean hts_findissystem(find_handle find) {
}
return 0;
}
hts_boolean hts_rename_over(const char *src, const char *dst) {
char csrc[CATBUFF_SIZE], cdst[CATBUFF_SIZE];
fconv(csrc, sizeof(csrc), src);
fconv(cdst, sizeof(cdst), dst);
if (RENAME(csrc, cdst) == 0)
return HTS_TRUE;
/* RENAME does not clobber an existing target on Windows. */
(void) UNLINK(cdst);
return RENAME(csrc, cdst) == 0 ? HTS_TRUE : HTS_FALSE;
}

View File

@@ -137,10 +137,6 @@ HTSEXT_API hts_boolean hts_findisdir(find_handle find);
HTSEXT_API hts_boolean hts_findisfile(find_handle find);
HTSEXT_API hts_boolean hts_findissystem(find_handle find);
/* Move src onto dst, replacing an existing dst; HTS_TRUE on success. Both
paths are fconv()'d. */
hts_boolean hts_rename_over(const char *src, const char *dst);
#endif
#endif

View File

@@ -57,9 +57,6 @@ Please visit our Website: http://www.httrack.com
/* opt->state.warc value meaning "open failed once, do not retry". */
#define WARC_DISABLED ((void *) ~(uintptr_t) 0)
/* Suffix of the in-progress archive when a previous one must survive it. */
#define WARC_TMP_SUFFIX ".tmp"
struct warc_writer {
FILE *f;
httrackp *opt; /* kept for close-time logging (warc_wacz_package) */
@@ -89,11 +86,6 @@ struct warc_writer {
char *base_path; /* resolved archive path minus .warc[.gz] suffix */
const char *base_ext; /* ".warc.gz" or ".warc" (static) */
char *arc_path; /* full single-file archive path (NULL under rotation) */
/* A re-run must not destroy an archive it cannot replace (#759). */
hts_boolean protect_prev; /* previous archive present: build in a temp */
hts_boolean opened; /* open completed; a failed one swaps nothing */
hts_boolean failed; /* a record or segment was lost: swap nothing */
uint64_t unbacked_revisits; /* revisits whose payload no file here holds */
char **page_lines; /* one JSON page line per 200 text/html response, owned */
size_t page_count;
size_t page_cap;
@@ -972,6 +964,16 @@ static zipFile wacz_zip_open(const char *path) {
return zipOpen2_64(path, 0 /*create*/, NULL, &ff);
}
/* Move src onto dst (UTF-8/Windows-safe); RENAME won't clobber on Windows, so
fall back to unlink+rename. Returns 0 on success. */
static int wacz_rename_over(const char *src, const char *dst) {
char cs[CATBUFF_SIZE], cd[CATBUFF_SIZE];
if (RENAME(fconv(cs, sizeof(cs), src), fconv(cd, sizeof(cd), dst)) == 0)
return 0;
(void) UNLINK(fconv(cd, sizeof(cd), dst));
return RENAME(fconv(cs, sizeof(cs), src), fconv(cd, sizeof(cd), dst));
}
/* Package the segment(s) + .cdx + a generated pages.jsonl into <base>.wacz at
crawl end (the archive file(s) and .cdx are already closed on disk). */
static void warc_wacz_package(warc_writer *w) {
@@ -1090,7 +1092,7 @@ static void warc_wacz_package(warc_writer *w) {
hts_log_print(w->opt, LOG_WARNING,
"WACZ: packaging failed, kept existing %s untouched",
waczpath);
} else if (!hts_rename_over(tmppath, waczpath)) {
} else if (wacz_rename_over(tmppath, waczpath) != 0) {
(void) UNLINK(fconv(catbuff, sizeof(catbuff), tmppath));
hts_log_print(w->opt, LOG_WARNING | LOG_ERRNO,
"WACZ: could not finalize %s", waczpath);
@@ -1137,17 +1139,13 @@ static int warc_emit(warc_writer *w, const char *type, const char *content_type,
never split a record, and never rotate a warcinfo (it opens a segment). */
if (w->max_size > 0 && w->seg_base != NULL && w->offset >= w->max_size &&
strcmp(type, "warcinfo") != 0) {
if (warc_rotate(w) != 0) {
w->failed = HTS_TRUE;
if (warc_rotate(w) != 0)
return -1;
}
}
/* F4: overflow-safe block length; http_hdr_len+sep is provably small. */
if (payload > (size_t) -1 - http_hdr_len - sep) {
w->failed = HTS_TRUE;
if (payload > (size_t) -1 - http_hdr_len - sep)
return -1;
}
block_len = http_hdr_len + sep + payload;
memset(&hdr, 0, sizeof(hdr));
@@ -1253,23 +1251,11 @@ static int warc_emit(warc_writer *w, const char *type, const char *content_type,
rc = 0;
done:
wbuf_free(&hdr);
if (rc != 0)
w->failed = HTS_TRUE; /* a truncated run must not replace a whole one */
return rc;
}
/* ---- segment rotation (--warc-max-size) ---- */
/* Path to open for the segment whose final path is `final`: that path itself,
or a sibling temp while a previous archive must survive until close. */
static const char *warc_open_path(warc_writer *w, const char *final, char *buf,
size_t bufsz) {
if (!w->protect_prev)
return final;
snprintf(buf, bufsz, "%s" WARC_TMP_SUFFIX, final);
return buf;
}
/* Emit the warcinfo that heads a segment; sets w->info_id for its records. */
static int warc_write_warcinfo_record(warc_writer *w) {
w->info_id[0] = '\0'; /* warcinfo itself carries no WARC-Warcinfo-ID */
@@ -1281,23 +1267,17 @@ static int warc_write_warcinfo_record(warc_writer *w) {
static int warc_rotate(warc_writer *w) {
char namebuf[HTS_URLMAXSIZE * 2];
char openbuf[HTS_URLMAXSIZE * 2 + sizeof(WARC_TMP_SUFFIX)];
char catbuff[CATBUFF_SIZE];
const unsigned next = w->seg + 1;
FILE *f;
snprintf(namebuf, sizeof(namebuf), "%s-%05u%s", w->seg_base, next,
w->seg_ext);
/* Open before advancing: w->seg must only ever name a segment that exists,
or close-time packaging and swapping would work on a missing file. */
f = FOPEN(fconv(catbuff, sizeof(catbuff),
warc_open_path(w, namebuf, openbuf, sizeof(openbuf))),
"wb");
if (f == NULL)
return -1;
if (w->f != NULL)
if (w->f != NULL) {
fclose(w->f);
w->f = f;
w->seg = next;
w->f = NULL;
}
w->seg++;
snprintf(namebuf, sizeof(namebuf), "%s-%05u%s", w->seg_base, w->seg,
w->seg_ext);
w->f = FOPEN(fconv(catbuff, sizeof(catbuff), namebuf), "wb");
if (w->f == NULL)
return -1;
w->offset = 0;
if (w->cdx_on) {
freet(w->cur_seg);
@@ -1354,7 +1334,6 @@ void warc_adopt_rawspool(htsblk *r, const char *tmpfile_path) {
warc_writer *warc_open(httrackp *opt, const char *path) {
warc_writer *w;
char namebuf[HTS_URLMAXSIZE * 2];
char openbuf[HTS_URLMAXSIZE * 2 + sizeof(WARC_TMP_SUFFIX)];
char catbuff[CATBUFF_SIZE];
wbuf info;
const char *robots;
@@ -1505,105 +1484,35 @@ warc_writer *warc_open(httrackp *opt, const char *path) {
path = namebuf;
}
if (w->max_size == 0 && (w->arc_path = strdupt(path)) == NULL) {
warc_close(w);
return NULL;
}
/* Set only once arc_path is recorded, so a half-built writer never swaps. */
w->protect_prev = fsize_utf8(path) > 0 ? HTS_TRUE : HTS_FALSE;
w->f = FOPEN(fconv(catbuff, sizeof(catbuff),
warc_open_path(w, path, openbuf, sizeof(openbuf))),
"wb");
w->f = FOPEN(fconv(catbuff, sizeof(catbuff), path), "wb");
if (w->f == NULL) {
warc_close(w);
return NULL;
}
if (w->cdx_on)
w->cur_seg = path_basename_dup(path);
if (w->wacz_on && w->max_size == 0)
w->arc_path = strdupt(path); /* single-file: package this exact path */
if (warc_write_warcinfo_record(w) != 0) {
warc_close(w);
return NULL;
}
w->opened = HTS_TRUE;
return w;
}
/* Final path of segment s (the run's only archive when rotation is off). */
static const char *warc_seg_path(warc_writer *w, unsigned s, char *buf,
size_t bufsz) {
if (w->max_size == 0)
return w->arc_path;
snprintf(buf, bufsz, "%s-%05u%s", w->seg_base, s, w->seg_ext);
return buf;
}
/* Swap this run's archive into place, unless it only holds revisits naming
bodies the previous archive still has and this one does not (#759).
HTS_FALSE: the previous archive was kept, so leave its .cdx and .wacz too. */
static hts_boolean warc_commit(warc_writer *w) {
char finalbuf[HTS_URLMAXSIZE * 2];
char tmpbuf[HTS_URLMAXSIZE * 2 + sizeof(WARC_TMP_SUFFIX)];
char catbuff[CATBUFF_SIZE];
const unsigned nseg = (w->max_size > 0) ? w->seg + 1 : 1;
hts_boolean swap;
unsigned s;
if (!w->protect_prev)
return HTS_TRUE; /* nothing was there to lose: written in place */
/* hts_rename_over unlinks its destination when the source is missing, so
every segment has to be on disk before the first rename. */
swap = w->opened && !w->failed && w->unbacked_revisits == 0;
for (s = 0; s < nseg && swap; s++) {
snprintf(tmpbuf, sizeof(tmpbuf), "%s" WARC_TMP_SUFFIX,
warc_seg_path(w, s, finalbuf, sizeof(finalbuf)));
swap = fsize_utf8(tmpbuf) > 0;
}
if (swap) {
for (s = 0; s < nseg; s++) {
const char *final = warc_seg_path(w, s, finalbuf, sizeof(finalbuf));
snprintf(tmpbuf, sizeof(tmpbuf), "%s" WARC_TMP_SUFFIX, final);
if (!hts_rename_over(tmpbuf, final)) {
hts_log_print(w->opt, LOG_ERROR | LOG_ERRNO,
"WARC: could not replace %s", final);
return HTS_FALSE;
}
}
return HTS_TRUE;
}
for (s = 0; s < nseg; s++) {
snprintf(tmpbuf, sizeof(tmpbuf), "%s" WARC_TMP_SUFFIX,
warc_seg_path(w, s, finalbuf, sizeof(finalbuf)));
(void) UNLINK(fconv(catbuff, sizeof(catbuff), tmpbuf));
}
if (w->unbacked_revisits > 0)
hts_log_print(
w->opt, LOG_ERROR,
"WARC: this pass revisited %llu URL(s) without re-downloading them, so "
"its archive would name bodies no file holds; kept the previous %s "
"(re-run with -C0, or --warc-file with a name of its own)",
(unsigned long long) w->unbacked_revisits,
warc_seg_path(w, 0, finalbuf, sizeof(finalbuf)));
return HTS_FALSE;
}
void warc_close(warc_writer *w) {
size_t i;
if (w == NULL)
return;
warc_cdx_flush(w); /* sort + write <base>.cdx before tearing down */
if (w->f != NULL)
fclose(w->f);
w->f = NULL;
if (warc_commit(w)) {
warc_cdx_flush(w); /* sort + write <base>.cdx beside the archive */
#if HTS_USEOPENSSL
if (w->wacz_on) /* package once the segment(s) + .cdx are closed on disk */
warc_wacz_package(w);
if (w->wacz_on) /* package once the segment(s) + .cdx are closed on disk */
warc_wacz_package(w);
#endif
}
if (w->seen != NULL)
coucal_delete(&w->seen);
for (i = 0; i < w->cdx_count; i++)
@@ -1684,8 +1593,6 @@ int warc_write_transaction(warc_writer *w, const char *target_uri,
if (is_update_unchanged) {
is_revisit = 1;
profile = "http://netpreserve.org/warc/1.1/revisit/server-not-modified";
/* Served from cache: the payload sits in the previous archive, not here. */
w->unbacked_revisits++;
} else if (have_pdig && w->seen != NULL) {
void *prev = NULL;
if (coucal_read_pvoid(w->seen, pdig, &prev) && prev != NULL) {

View File

@@ -101,8 +101,8 @@ static void htsweb_sig_brpipe(int code) {
/* ignore */
}
/* Threads that never return; no wait may count on them draining. */
static int nonjoinable_threads = 0;
/* Number of background threads */
static int background_threads = 0;
/* Server/client ping handling */
static htsmutex pingMutex = HTSMUTEX_INIT;
@@ -224,7 +224,9 @@ int main(int argc, char *argv[]) {
#ifdef HTS_USESWF
smallserver_setkey("USESWF", "1");
#endif
#ifdef HTS_USEZLIB
smallserver_setkey("USEZLIB", "1");
#endif
#ifdef _WIN32
smallserver_setkey("WIN32", "1");
#endif
@@ -297,19 +299,15 @@ int main(int argc, char *argv[]) {
/* pinger */
if (parentPid > 0) {
if (hts_newthread(client_ping, (void *) (uintptr_t) parentPid) == 0) {
#ifndef _WIN32
nonjoinable_threads++; /* client_ping() only ever leaves through exit() */
#endif
}
hts_newthread(client_ping, (void *) (uintptr_t) parentPid);
background_threads++; /* Do not wait for this thread! */
smallserver_setpinghandler(pingHandler, NULL);
}
/* launch */
ret = help_server(argv[1], defaultPort, bindAddr);
/* Drain everything a mirror may still have in flight, the pinger aside. */
htsthread_wait_n(nonjoinable_threads);
htsthread_wait_n(background_threads - 1);
hts_uninit();
#ifdef _WIN32
@@ -384,6 +382,7 @@ void webhttrack_main(char *cmd) {
commandRunning = 1;
DEBUG(fprintf(stderr, "commandRunning=1\n"));
hts_newthread(back_launch_cmd, (void *) strdup(cmd));
background_threads++; /* Do not wait for this thread! */
}
void webhttrack_lock(void) {
@@ -424,8 +423,8 @@ static int webhttrack_runmain(httrackp * opt, int argc, char **argv) {
/* Rock'in! */
ret = hts_main2(argc, argv, opt);
/* Wait for pending threads to finish; the pinger and this thread stay. */
htsthread_wait_n(nonjoinable_threads + 1);
/* Wait for pending threads to finish */
htsthread_wait_n(background_threads);
return ret;
}

View File

@@ -40,6 +40,7 @@ Please visit our Website: http://www.httrack.com
#include "htscodec.h"
#include "htszlib.h"
#if HTS_USEZLIB
/* zlib */
/*
#include <zlib.h>
@@ -273,3 +274,4 @@ const char *hts_get_zerror(int err) {
break;
}
}
#endif

View File

@@ -1,7 +0,0 @@
#!/bin/bash
#
# an empty current path underflowed htsAddLink's codebase walk (#730).
set -euo pipefail
httrack -O /dev/null -#test=addlink | grep -q "addlink self-test OK"

View File

@@ -1,12 +0,0 @@
#!/bin/bash
#
# structcheck(): the path-length guard, and the <name>.txt rename that once
# built its target with an unbounded sprintf (#745).
set -euo pipefail
dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT
httrack -O /dev/null -#test=structcheck "$dir" |
grep -q "structcheck self-test OK"

View File

@@ -1,28 +0,0 @@
#!/bin/bash
#
set -euo pipefail
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/httrack_threadwait_st.XXXXXX") || exit 1
trap 'rm -rf "$tmpdir"' EXIT HUP INT QUIT PIPE TERM
# No pipe into grep: SIGPIPE would mask a failing exit status.
expect_ok() {
local label="$1" out
shift
out=$("$@" 2>&1) || {
echo "FAIL: ${label} exited non-zero: ${out}"
exit 1
}
case "$out" in
*"${label}: OK"*) ;;
*)
echo "FAIL: ${out}"
exit 1
;;
esac
}
# A thread is outstanding from the moment hts_newthread() returns, so a wait
# that follows the spawn joins it, and wait_n(n) still leaves n behind (#747).
expect_ok "threadwait self-test" httrack -O "${tmpdir}/o1" -#test=threadwait

View File

@@ -18,7 +18,6 @@ set -euo pipefail
bash "$top_srcdir/tests/local-crawl.sh" \
--rerun-args '--update -M400000' \
--log-found 'More than 400000 bytes have been transferred.. giving up' \
--log-not-found 'could not restore' \
--found bigtrunc/slow.bin \
--file-min-bytes bigtrunc/slow.bin 655360 \
--file-min-bytes bigtrunc/fast.bin 655360 \

View File

@@ -79,29 +79,6 @@ done
# 65535 is a valid port the old "< 65535" bound refused
web_accepted 65535
# --- htsserver returns from main() instead of blocking at exit -------------
# The exit wait must exclude exactly the threads that never return (#753).
# $tmp has no lang.def, so the server fails right after announcing and main()
# reaches the wait on its own; rc, not EXITED, carries the verdict.
web_exits() {
local rc=0
: >"$tmp/exit.log"
run_with_timeout 30 htsserver "$tmp" "$@" >"$tmp/exit.log" 2>&1 || rc=$?
grep -q "^EXITED" "$tmp/exit.log" ||
! echo "FAIL: #753: htsserver ${*:-(no options)} never finished serving" || exit 1
# exactly 1, not merely "not 124": a crash or an assertf abort also escapes
# the wait, and would otherwise read as a pass
test "$rc" -eq 1 ||
! echo "FAIL: #753: htsserver ${*:-(no options)} exited $rc, want 1" || exit 1
}
# no pinger: the excluded count went negative
web_exits
# a pinger, which never returns and so must stay excluded
web_exits --ppid $$
# --- proxytrack <proxy-addr:port> <ICP-addr:port> --------------------------
# A bad argument falls through to the usage screen; it had no range check at
# all, so 65616 quietly listened on port 80. A valid one binds and blocks.

View File

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

View File

@@ -5,17 +5,11 @@
# real gate: STORE-mode entries, the fixed layout, recomputed sha256 digests and
# the datapackage-digest chain (plus py-wacz/pywb when importable). The crawl
# skips cleanly on a build without OpenSSL (no conformant SHA-256 -> no package).
#
# The cacheless (-C0) second pass repackages over the .wacz the first one left:
# the clobber, not the happy rename, is what breaks when a platform refuses to
# rename onto an existing file (#726). A cache-served pass keeps the previous
# package instead, which is 97_local-warc-update-keep.
set -eu
: "${top_srcdir:=..}"
bash "$top_srcdir/tests/local-crawl.sh" --errors 0 --wacz-validate \
--rerun-args '-C0' --log-not-found 'could not finalize' \
--found 'mini304/index.html' --found 'mini304/page.html' \
httrack 'BASEURL/mini304/index.html' --warc-file warc-out --wacz

View File

@@ -1,15 +0,0 @@
#!/bin/bash
#
# A cache-served second pass fetches no bodies, so it must leave the WARC, CDXJ
# and WACZ of the first pass untouched (#759). The kept package is re-validated
# to prove it still replays, not just that a same-named file is still there.
# The one Error: line is the keep notice; a second means the pass also failed.
set -eu
: "${top_srcdir:=..}"
bash "$top_srcdir/tests/local-crawl.sh" --errors 1 --archive-kept-on-rerun \
--rerun --wacz-validate --log-found 'WARC: this pass revisited' \
--found 'mini304/index.html' --found 'mini304/page.html' \
httrack 'BASEURL/mini304/index.html' --warc-file warc-out --wacz

View File

@@ -1,24 +0,0 @@
#!/bin/bash
#
# The same keep/replace rule under --warc-max-size, where the archive is several
# segments: a cache-served pass must leave every one alone, a cacheless pass must
# swap every one in. Half a swap is the corruption one rename loop can produce,
# and only a rotated archive exposes it (#759). --archive-min-files pins the two
# segments plus the .cdx, so a shrinking crawl cannot quietly stop rotating.
set -eu
: "${top_srcdir:=..}"
# The one Error: line is the keep notice; a second means the pass also failed.
bash "$top_srcdir/tests/local-crawl.sh" --errors 1 --archive-kept-on-rerun \
--archive-min-files 3 --rerun --log-found 'WARC: this pass revisited' \
--found 'mini304/index.html' --found 'mini304/page.html' \
httrack 'BASEURL/mini304/index.html' --warc-file warc-out --warc-cdx \
--warc-max-size 600
bash "$top_srcdir/tests/local-crawl.sh" --errors 0 --archive-replaced-on-rerun \
--archive-min-files 3 --rerun-args '-C0' \
--found 'mini304/index.html' --found 'mini304/page.html' \
httrack 'BASEURL/mini304/index.html' --warc-file warc-out --warc-cdx \
--warc-max-size 600

View File

@@ -30,7 +30,6 @@ TEST_EXTENSIONS = .test
TEST_LOG_COMPILER = $(BASH)
TESTS = \
00_runnable.test \
01_engine-addlink.test \
01_engine-changes.test \
01_engine-charset.test \
01_engine-cmdline.test \
@@ -82,9 +81,7 @@ TESTS = \
01_engine-status.test \
01_engine-stripquery.test \
01_engine-strsafe.test \
01_engine-structcheck.test \
01_engine-syscharset.test \
01_engine-threadwait.test \
01_engine-topindex.test \
01_engine-urlhack.test \
01_engine-unescape-bounds.test \
@@ -198,8 +195,6 @@ TESTS = \
93_local-changes.test \
94_local-single-file.test \
95_local-sitemap.test \
96_local-refetch-keep.test \
97_local-warc-update-keep.test \
98_local-warc-segments.test
96_local-refetch-keep.test
CLEANFILES = check-network_sh.cache

View File

@@ -34,12 +34,6 @@
# httrack via --cookies-file, to exercise preloaded cookies.
# --rerun-dead re-runs with the server stopped: the no-data rollback must
# restore the previous hts-cache generation byte-identical.
# --archive-kept-on-rerun: the second pass must leave the first pass's
# .warc[.gz]/.cdx/.wacz byte-identical, having no bodies to replace them (#759).
# --archive-replaced-on-rerun is its mirror: every one of them must have been
# rewritten. Both also require no *.tmp left behind, and take an optional
# --archive-min-files N guarding against a scenario that silently stopped
# producing the segments it means to check.
set -u
@@ -60,14 +54,9 @@ outdir_intl=
rerun=
rerun_args=
rerun_dead=
archive_kept=
archive_replaced=
archive_min_files=0
tmpdir=
serverpid=
crawlpid=
wacz_poisoned=
wacz_poison="stale-wacz-that-a-second-pass-must-replace"
function warning {
echo "** $*" >&2
@@ -127,13 +116,6 @@ while test "$pos" -lt "$nargs"; do
--debug) verbose=1 ;;
--rerun) rerun=1 ;; # run httrack a second time (update pass) before auditing
--rerun-dead) rerun_dead=1 ;; # re-run with the server stopped (cache rollback)
# the second pass must leave the first pass's archive files untouched
--archive-kept-on-rerun) archive_kept=1 ;;
--archive-replaced-on-rerun) archive_replaced=1 ;; # ...or rewrite all of them
--archive-min-files)
pos=$((pos + 1))
archive_min_files="${args[$pos]}"
;;
# validate the produced .warc.gz (see the validation block near the end)
--warc-validate) warc_validate=1 ;;
# validate the produced .wacz package (stdlib, plus py-wacz/pywb if present)
@@ -290,30 +272,6 @@ if test -n "$warc_validate"; then
test -z "$w1" || cp "$w1" "${tmpdir}/warc-pass1.gz"
fi
# Snapshot the archive files the second pass must keep (or must replace).
declare -a kept_files=()
if test -n "${archive_kept}${archive_replaced}"; then
while read -r f; do
test -n "$f" || continue
cp "$f" "${tmpdir}/kept-${#kept_files[@]}" || die "could not snapshot $f"
kept_files+=("$f")
done < <(find "$mirrorroot" -maxdepth 2 \
\( -name '*.warc.gz' -o -name '*.warc' -o -name '*.cdx' -o -name '*.wacz' \) \
2>/dev/null | sort)
test "${#kept_files[@]}" -gt 0 ||
die "the first pass produced no archive to compare against"
test "${#kept_files[@]}" -ge "$archive_min_files" ||
die "only ${#kept_files[@]} archive file(s), wanted $archive_min_files: ${kept_files[*]}"
fi
# Poison the first-pass .wacz when a second pass follows: repackaging moves the
# new archive over it, so the marker must be gone afterwards (#726). Poisoning
# beats comparing the two packages, which can come out byte-identical.
if test -z "$archive_kept" && test -n "$wacz_validate" && test -n "${rerun}${rerun_args}"; then
wacz_poisoned=$(find "$mirrorroot" -maxdepth 2 -name '*.wacz' 2>/dev/null | sort | tail -n1)
test -z "$wacz_poisoned" || echo "$wacz_poison" >"$wacz_poisoned"
fi
# --- optional second pass: re-mirror into the same dir (cache/update path) ----
if test -n "$rerun"; then
info "re-running httrack (update pass)"
@@ -358,29 +316,6 @@ if test -n "$rerun_args"; then
result "OK (second pass)"
fi
# --- optional: did the second pass keep, or replace, the whole archive? ------
if test "${#kept_files[@]}" -gt 0; then
i=0
for f in "${kept_files[@]}"; do
if test -n "$archive_kept"; then
info "checking the second pass kept $(basename "$f")"
cmp -s "${tmpdir}/kept-${i}" "$f" ||
die "$(basename "$f") was rewritten: the previous archive was destroyed"
else
info "checking the second pass replaced $(basename "$f")"
cmp -s "${tmpdir}/kept-${i}" "$f" &&
die "$(basename "$f") still holds the first pass's bytes"
fi
result "OK"
i=$((i + 1))
done
# A leftover in-progress file is as bad: the next pass would silently eat it.
info "checking no in-progress archive was left behind"
leftover=$(find "$mirrorroot" -maxdepth 2 \( -name '*.warc.gz.tmp' -o -name '*.warc.tmp' \) 2>/dev/null | head -n1)
test -z "$leftover" || die "left behind $leftover"
result "OK"
fi
# --- optional dead pass: server stopped, the cache must survive the rollback --
if test -n "$rerun_dead"; then
zip="${out}/hts-cache/new.zip"
@@ -452,18 +387,14 @@ if test -n "$warc_validate"; then
die "fresh WARC validation failed"
result "OK"
# After an update pass the unchanged assets must be revisits, in an archive
# of the pass's own (WARC_VALIDATE_UPDATE): a revisit-only pass never
# replaces the archive holding the bodies it would strand (#759).
if test -n "${WARC_VALIDATE_UPDATE:-}"; then
upd=$(find "$mirrorroot" -maxdepth 2 -name "$WARC_VALIDATE_UPDATE" 2>/dev/null | head -n1)
test -n "$upd" || die "no $WARC_VALIDATE_UPDATE produced under $mirrorroot"
# Final file: after an update pass the unchanged assets must be revisits.
if test -n "$rerun"; then
declare -a revargs=(--expect-revisit)
for sub in ${WARC_VALIDATE_NORESP:-}; do
revargs+=(--no-response-for "$sub")
done
info "validating update WARC (revisits)"
"$python" "$validator" "$(nativepath "$upd")" "${revargs[@]}" >&2 ||
"$python" "$validator" "$(nativepath "$warc")" "${revargs[@]}" >&2 ||
die "update WARC validation failed"
result "OK"
fi
@@ -485,12 +416,6 @@ if test -n "$wacz_validate"; then
fi
die "no .wacz file produced under $mirrorroot"
fi
if test -n "$wacz_poisoned"; then
info "checking the second pass replaced the .wacz"
grep -q "$wacz_poison" "$wacz_poisoned" 2>/dev/null &&
die "stale .wacz kept: $wacz_poisoned"
result "OK"
fi
validator=$(nativepath "${testdir}/wacz-validate.py")
info "validating WACZ package"
"$python" "$validator" "$(nativepath "$wacz")" >&2 || die "WACZ validation failed"