Compare commits

..

2 Commits

Author SHA1 Message Date
Xavier Roche
1cfd022cf3 Condense the -o1 control comment to one line
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <xroche@gmail.com>
2026-07-27 14:16:29 +02:00
Xavier Roche
f98a3d1e63 The -o help text promises a generated error page the engine never builds
`-o` only decides whether the error page the server sent survives: `store_errpage`
keeps `r.adr` alive so the normal save path writes it, and the `-o0` arm frees it.
Nothing anywhere builds a stand-in body. The one block that would have was dead
since the 3.20.2 import and was removed in #783.

Reword the help line, the man page and fcguide's two `-o` prose blocks to say the
server's error page is saved rather than generated, and extend 23_local-errpage
so the mirrored 404 has to carry the server's own body.

Closes #787

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-27 13:39:10 +02:00
60 changed files with 180 additions and 984 deletions

View File

@@ -627,12 +627,7 @@ jobs:
set -euo pipefail
git fetch --no-tags origin \
"+refs/heads/${{ github.base_ref }}:refs/remotes/origin/${{ github.base_ref }}"
# Merge base, not the branch tip: master moves during a run, and a
# tip-relative diff blames its new C on this PR.
if ! base="$(git merge-base "origin/${{ github.base_ref }}" HEAD)"; then
echo "::error::no merge base with origin/${{ github.base_ref }}; cannot scope the check."
exit 1
fi
base="origin/${{ github.base_ref }}"
set +e
diff="$(git clang-format --binary clang-format-19 --style=file \
--diff --extensions c,h "$base")"
@@ -674,8 +669,6 @@ jobs:
git fetch --no-tags origin \
"+refs/heads/${{ github.base_ref }}:refs/remotes/origin/${{ github.base_ref }}"
base="origin/${{ github.base_ref }}"
# Three dots: merge-base scoped, so commits landing on master mid-run
# are not counted as this PR's.
changed="$(git diff --name-only "$base"...HEAD)"
has() { printf '%s\n' "$changed" | grep -qx "$1"; }
if has man/httrack.1 && ! has html/httrack.man.html; then

View File

@@ -262,9 +262,8 @@ jobs:
# crange pending #581;
# webdav-mime needs a reapable background listener, which MSYS cannot give it;
# badmtime needs a filesystem that stores an mtime past gmtime's range;
# single-file ends on a GUI half needing htsserver, which this job does not build;
# update-304-leak needs a LeakSanitizer build, which MSVC has no equivalent of.
expected_skips=" 01_engine-footer-overflow.test 100_local-purge-longpath.test 114_local-update-304-leak.test 48_local-crange-memresume.test 71_local-crange-repaircache.test 79_local-proxytrack-webdav-mime.test 88_local-proxytrack-badmtime.test 94_local-single-file.test"
# single-file ends on a GUI half needing htsserver, which this job does not build.
expected_skips=" 01_engine-footer-overflow.test 100_local-purge-longpath.test 48_local-crange-memresume.test 71_local-crange-repaircache.test 79_local-proxytrack-webdav-mime.test 88_local-proxytrack-badmtime.test 94_local-single-file.test"
# First, or the deadline reads as an unexplained shortfall in the gates below.
[ "$deadline" -eq 0 ] || { echo "::error::suite did not finish within ${suite_deadline}s"; exit 1; }
[ "$pass" -ge 90 ] || { echo "::error::only $pass tests passed ($skip skipped)"; exit 1; }

View File

@@ -30,14 +30,12 @@ the operational checklist: toolchain, invariants, and how to ship a change.
failing cleanup command becomes the test's exit status (#773). Keep the other
signals on their own `trap` line, or errexit stays off for the rest of the run.
The guard also resets `$?`, so save it first if teardown reads it.
- Never pipe into `grep -q`: it exits on the first match, so whatever the
producer had left to write takes SIGPIPE, and under `pipefail` that becomes
the pipeline's status. `cmd | grep -q M && fail` then never fires and a probe
that proved nothing reads as "marker absent"; `cmd | grep -q M || fail` fails
a test whose marker was present. bash issues one `write()` per line, so any
match that is not on the last line is exposed. Capture the reply, assert the
status line it must carry (an empty, truncated or redirected one is
marker-free too), then match with a here-string: `grep -q M <<<"$reply"`.
- Never assert with `cmd | grep -q MARKER && fail`. Under `pipefail` the
pipeline is non-zero both when `cmd` fails and when `grep -q` matches early
and SIGPIPEs it, so the `&&` never fires and a probe that proved nothing reads
as "marker absent". Capture the reply, assert the status line it must carry
(an empty, truncated or redirected one is marker-free too), then match with a
here-string.
## Hard invariants
- **Generated autotools files are NOT in git.** `configure`, every

View File

@@ -391,56 +391,6 @@ int back_selftest_slot_swap(void) {
CHECK(0, "the dummy test slot");
#undef CHECK
/* The swap round-trip must not lose the size of a slot whose body is already
at url_sav, or the link writer blanks the file (#797). */
{
static const char body[] = "swapped body";
int c;
for (c = 0; c < 2; c++) {
const hts_boolean inmemory = c == 0 ? HTS_TRUE : HTS_FALSE;
FILE *const fp = tmpfile();
lien_back *copy = NULL;
memset(&back, 0, sizeof(back));
back.status = STATUS_READY;
strcpybuff(back.url_sav, "/tmp/httrack-selftest.bin");
back.r.size = (LLint) sizeof(body) - 1;
if (inmemory) {
back.r.adr = strdupt(body);
}
if (fp == NULL || back_serialize(fp, &back) != 0 ||
fseek(fp, 0, SEEK_SET) != 0 || back_unserialize(fp, &copy) != 0) {
fprintf(stderr, "backswap: round-trip failed for a %s slot\n",
inmemory ? "buffered" : "direct-to-disk");
err = 1;
} else {
if (copy->r.size != back.r.size) {
fprintf(stderr,
"backswap: %s slot came back with size " LLintP
", expected " LLintP "\n",
inmemory ? "buffered" : "direct-to-disk", copy->r.size,
back.r.size);
err = 1;
}
if (inmemory && (copy->r.adr == NULL ||
memcmp(copy->r.adr, body, sizeof(body) - 1) != 0)) {
fprintf(stderr, "backswap: buffered slot lost its body\n");
err = 1;
}
if (!inmemory && copy->r.adr != NULL) {
fprintf(stderr, "backswap: direct-to-disk slot gained a body\n");
err = 1;
}
back_clear_entry(copy);
freet(copy);
}
if (fp != NULL)
fclose(fp);
freet(back.r.adr);
}
}
printf("backswap self-test: %s\n", err ? "FAIL" : "OK");
return err;
}
@@ -696,7 +646,7 @@ void back_refetch_backup(httrackp *opt, lien_back *const back) {
if (fexist_utf8(back->tmpfile))
hts_log_print(opt, LOG_WARNING, "replacing leftover backup %s",
back->tmpfile);
saved = hts_rename_over(opt, back->url_sav, back->tmpfile);
saved = hts_rename_over(back->url_sav, back->tmpfile);
}
if (!saved) {
hts_log_print(opt, LOG_WARNING | LOG_ERRNO,
@@ -737,7 +687,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(opt, back->tmpfile, back->url_sav))
if (!hts_rename_over(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);
@@ -868,7 +818,7 @@ int back_finalize(httrackp * opt, cache_back * cache, struct_back * sback,
"Read error when decompressing");
}
UNLINK(unpacked);
} else if (hts_rename_over(opt, unpacked, back[p].url_sav)) {
} else if (hts_rename_over(unpacked, back[p].url_sav)) {
/* The temp bypassed filecreate(), which is what chmods. */
#ifndef _WIN32
chmod(back[p].url_sav, HTS_ACCESS_FILE);
@@ -1223,13 +1173,6 @@ void back_connxfr(htsblk * src, htsblk * dst) {
src->debugid = 0;
}
/* Release the buffers a response owns. The connection members are left alone:
back_connxfr() moves those, and the file handles are closed elsewhere. */
static void back_free_response(htsblk *r) {
deleteaddr(r);
warc_free_request(r);
}
void back_move(lien_back * src, lien_back * dst) {
memcpy(dst, src, sizeof(lien_back));
memset(src, 0, sizeof(lien_back));
@@ -1323,10 +1266,7 @@ int back_unserialize(FILE * fp, lien_back ** dst) {
(*dst)->r.ssl_con = NULL;
#endif
if (back_data_unserialize(fp, (void **) &(*dst)->r.adr, &size) == 0) {
/* A bodyless slot already wrote its bytes to url_sav (FTP, direct to
disk); zeroing r.size makes the writer blank that file (#797). */
if ((*dst)->r.adr != NULL)
(*dst)->r.size = size;
(*dst)->r.size = size;
(*dst)->r.headers = NULL;
if (back_string_unserialize(fp, &(*dst)->r.headers) == 0)
return 0; /* ok */
@@ -1765,7 +1705,10 @@ int back_clear_entry(lien_back * back) {
back->r.soc = INVALID_SOCKET;
}
back_free_response(&back->r);
if (back->r.adr != NULL) { // reste un bloc à désallouer
freet(back->r.adr);
back->r.adr = NULL;
}
if (back->chunk_adr != NULL) { // reste un bloc à désallouer
freet(back->chunk_adr);
back->chunk_adr = NULL;
@@ -1778,6 +1721,12 @@ int back_clear_entry(lien_back * back) {
(void) unlink(back->tmpfile);
back->tmpfile = NULL;
}
// headers
if (back->r.headers != NULL) {
freet(back->r.headers);
back->r.headers = NULL;
}
warc_free_request(&back->r);
// Tout nettoyer
memset(back, 0, sizeof(lien_back));
back->r.soc = INVALID_SOCKET;
@@ -4082,9 +4031,6 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
memset(&tmp, 0, sizeof(tmp));
back_connxfr(&back[i].r, &tmp);
/* the cache entry overwrites the whole struct, so drop
what the 304 response still owns first (#782) */
back_free_response(&back[i].r);
back[i].r =
cache_read(opt, cache, back[i].url_adr, back[i].url_fil,
back[i].url_sav, back[i].location_buffer);

View File

@@ -985,7 +985,6 @@ void cache_init(cache_back * cache, httrackp * opt) {
StringBuff(opt->path_log),
"hts-cache/new.zip")))) { // a previous cache exists.. rename it
if (!hts_rename_over(
opt,
fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
StringBuff(opt->path_log), "hts-cache/new.zip"),
fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),

View File

@@ -483,19 +483,16 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
back->r.totalsize = size;
}
}
/* Only over a copy back_add() judged partial: on --update every
mirrored file exists, and resuming a complete one splices the
old body into the new (#798). */
if (back->range_req_size > 0 && (transfer_list == 0)) {
// REST?
if (fexist(back->url_sav) && (transfer_list == 0)) {
strcpybuff(back->info, "rest");
snprintf(line, sizeof(line), "REST " LLintP,
(LLint) back->range_req_size);
snprintf(line, sizeof(line), "REST " LLintP, (LLint) fsize(back->url_sav));
send_line(soc_ctl, line);
get_ftp_line(soc_ctl, line, sizeof(line), timeout);
_CHECK_HALT_FTP;
if ((line[0] == '3') || (line[0] == '2')) { // ok
rest_understood = 1;
} // else never mind
} // sinon tant pis
}
} // sinon tant pis
}
@@ -620,12 +617,9 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
// Ok, connexion initiée
//
if (soc_dat != INVALID_SOCKET) {
if (rest_understood) { // REST sent and understood
if (rest_understood) { // REST envoyée et comprise
file_notify(opt, back->url_adr, back->url_fil, back->url_sav, 0, 1,
0);
/* The bytes already on disk count too, or the completeness check
below rejects every resumed transfer (#798). */
back->r.size = back->range_req_size;
back->r.fp = fileappend(&opt->state.strc, back->url_sav);
} else {
file_notify(opt, back->url_adr, back->url_fil, back->url_sav, 1, 1,

View File

@@ -2703,8 +2703,17 @@ void hts_now_iso8601(char out[32]) {
time_t t = time(NULL);
struct tm tmv;
if (!hts_gmtime(t, &tmv))
#if defined(_WIN32)
struct tm *g = gmtime(&t);
if (g != NULL)
tmv = *g;
else
memset(&tmv, 0, sizeof(tmv));
#else
if (gmtime_r(&t, &tmv) == NULL)
memset(&tmv, 0, sizeof(tmv));
#endif
strftime(out, 32, "%Y-%m-%dT%H:%M:%SZ", &tmv);
}
@@ -5781,8 +5790,7 @@ HTSEXT_API void hts_log_vprint(httrackp * opt, int type, const char *format, va_
if (hts_log_print_callback != NULL) {
va_list args_copy;
va_copy(args_copy, args);
/* the copy, so the vfprintf() below still has an unread list */
hts_log_print_callback(opt, type, format, args_copy);
hts_log_print_callback(opt, type, format, args);
va_end(args_copy);
}
if (opt != NULL && opt->log != NULL) {

View File

@@ -157,19 +157,6 @@ struct t_dnscache {
char host_addr[HTS_MAXADDRNUM][HTS_MAXADDRLEN];
};
/* Break t down as UTC into the caller's buffer, HTS_FALSE if that failed.
gmtime()'s static is shared, and both the engine and ProxyTrack convert on
worker threads. */
static HTS_INLINE HTS_UNUSED hts_boolean hts_gmtime(time_t t,
struct tm *tmbuf) {
#ifdef _WIN32
/* Microsoft's gmtime_s takes the destination first, unlike C11 Annex K. */
return gmtime_s(tmbuf, &t) == 0 ? HTS_TRUE : HTS_FALSE;
#else
return gmtime_r(&t, tmbuf) != NULL ? HTS_TRUE : HTS_FALSE;
#endif
}
/* Library internal definictions */
#ifdef HTS_INTERNAL_BYTECODE

View File

@@ -2622,74 +2622,6 @@ static int st_savename(httrackp *opt, int argc, char **argv) {
return 0;
}
static char st_log_callback_seen[256];
static void st_log_callback(httrackp *opt, int type, const char *format,
va_list args) {
(void) opt;
(void) type;
(void) vsnprintf(st_log_callback_seen, sizeof(st_log_callback_seen), format,
args);
}
/* The callback must not consume the va_list the log file's vfprintf() needs. */
static int st_logcallback(httrackp *opt, int argc, char **argv) {
static const char want[] = "42 sentinel";
static const char want_filtered[] = "7 filtered";
char BIGSTK seen[sizeof(st_log_callback_seen)];
char BIGSTK line[256];
FILE *fp;
int rc = 1;
(void) argc;
(void) argv;
fp = tmpfile();
if (fp == NULL) {
fprintf(stderr, "logcallback: tmpfile() failed\n");
return 1;
}
opt->log = fp;
opt->debug = LOG_NOTICE;
st_log_callback_seen[0] = '\0';
hts_set_log_vprint_callback(st_log_callback);
hts_log_print(opt, LOG_NOTICE, "%d %s", 42, "sentinel");
hts_set_log_vprint_callback(NULL);
opt->log = NULL;
strcpybuff(seen, st_log_callback_seen);
rewind(fp);
if (fgets(line, (int) sizeof(line), fp) == NULL) {
fprintf(stderr, "logcallback: log file is empty, nothing was written\n");
fclose(fp);
return 1;
}
fclose(fp);
/* The callback runs above the level filter and without a log file at all;
the front-ends that install one usually have no opt->log open. */
st_log_callback_seen[0] = '\0';
hts_set_log_vprint_callback(st_log_callback);
hts_log_print(opt, LOG_DEBUG, "%d %s", 7, "filtered");
hts_set_log_vprint_callback(NULL);
/* Same arguments both ways; the file line carries a level prefix. */
if (strcmp(seen, want) != 0)
fprintf(stderr, "logcallback: callback got '%s' want '%s'\n", seen, want);
else if (strstr(line, want) == NULL)
fprintf(stderr, "logcallback: log file got '%s' want it to carry '%s'\n",
line, want);
else if (strcmp(st_log_callback_seen, want_filtered) != 0)
fprintf(stderr, "logcallback: unfiltered callback got '%s' want '%s'\n",
st_log_callback_seen, want_filtered);
else
rc = 0;
if (rc == 0)
printf("logcallback self-test OK\n");
return rc;
}
/* an empty fil started htsAddLink's codebase walk before the buffer (#730) */
static int st_addlink(httrackp *opt, int argc, char **argv) {
htsmoduleStruct BIGSTK str;
@@ -6140,9 +6072,10 @@ static hts_boolean ro_is(const char *path, const char *data) {
}
// -#test=renameover <dir>: hts_rename_over() must replace an existing dst and
// never lose one it did not replace (#779, #790). Which half is live depends on
// never delete one it did not replace (#779). Which half is live depends on
// what rename() does to an existing target, so probe that and name the regime.
static int st_renameover(httrackp *opt, int argc, char **argv) {
(void) opt;
if (argc < 1) {
fprintf(stderr, "renameover: needs a writable base dir\n");
return 1;
@@ -6171,7 +6104,7 @@ static int st_renameover(httrackp *opt, int argc, char **argv) {
ro_put(dst, "old");
if (replaceable) {
/* An existing dst must still be replaced: the unlink is for this. */
if (!hts_rename_over(opt, src, dst)) {
if (!hts_rename_over(src, dst)) {
fprintf(stderr, "renameover: replacing an existing dst failed: %s\n",
strerror(errno));
err++;
@@ -6181,7 +6114,7 @@ static int st_renameover(httrackp *opt, int argc, char **argv) {
}
} else {
/* A failure the unlink cannot fix must leave dst as it was. */
if (hts_rename_over(opt, src, dst)) {
if (hts_rename_over(src, dst)) {
fprintf(stderr, "renameover: an unfixable failure reported success\n");
err++;
}
@@ -6191,36 +6124,10 @@ static int st_renameover(httrackp *opt, int argc, char **argv) {
}
}
/* A directory in the way is not something the caller asked to replace: it
must be refused, never parked aside and orphaned. */
(void) UNLINK(dst);
ro_put(src, "new");
if (MKDIR(dst) == 0) {
char parked[sizeof(dst) + 16];
snprintf(parked, sizeof(parked), "%s.hts-old0", dst);
if (hts_rename_over(opt, src, dst)) {
fprintf(stderr, "renameover: a directory at dst reported success\n");
err++;
}
if (!ro_is(src, "new")) {
fprintf(stderr, "renameover: a directory at dst consumed src\n");
err++;
}
/* RMDIR only succeeds on a directory that is there, so it doubles as the
probe: the parked name must not exist at all. */
if (RMDIR(parked) == 0 || fexist_utf8(parked)) {
fprintf(stderr, "renameover: a directory at dst was parked aside\n");
err++;
}
(void) RMDIR(dst);
}
(void) UNLINK(src);
/* A missing src must leave dst alone and report failure. */
(void) UNLINK(src);
ro_put(dst, "keep");
if (hts_rename_over(opt, src, dst)) {
if (hts_rename_over(src, dst)) {
fprintf(stderr, "renameover: a missing src reported success\n");
err++;
}
@@ -6231,88 +6138,11 @@ static int st_renameover(httrackp *opt, int argc, char **argv) {
/* Same, with dst absent too: nothing to lose, still a failure. */
(void) UNLINK(dst);
if (hts_rename_over(opt, src, dst)) {
if (hts_rename_over(src, dst)) {
fprintf(stderr, "renameover: a missing src and dst reported success\n");
err++;
}
/* The aside fallback, driven directly: a clobbering rename() never reaches
it. Skipped in the refused regime, where no rename at all succeeds. */
if (replaceable) {
char aside[sizeof(dst) + 16], keep[sizeof(dst) + 16];
snprintf(aside, sizeof(aside), "%s.hts-old0", dst);
snprintf(keep, sizeof(keep), "%s.hts-old1", dst);
(void) UNLINK(aside);
(void) UNLINK(keep);
ro_put(src, "new");
ro_put(dst, "old");
if (!hts_rename_over_aside_selftest(opt, src, dst)) {
fprintf(stderr, "renameover: the aside fallback failed: %s\n",
strerror(errno));
err++;
} else if (!ro_is(dst, "new") || fexist_utf8(src) || fexist_utf8(aside)) {
fprintf(stderr, "renameover: the aside fallback did not replace dst\n");
err++;
}
/* #790: the retry fails (no src). The old content must survive, back at dst
or, when the move back fails too, under the parked name it is logged as.
Name the outcome so a leg cannot pass having tested the other one. */
(void) UNLINK(src);
ro_put(dst, "old");
if (hts_rename_over_aside_selftest(opt, src, dst)) {
fprintf(stderr, "renameover: a failed aside retry reported success\n");
err++;
}
if (ro_is(dst, "old") && !fexist_utf8(aside)) {
printf("renameover: restore back\n");
} else if (ro_is(aside, "old") && !fexist_utf8(dst)) {
printf("renameover: restore parked\n");
(void) UNLINK(aside);
ro_put(dst, "old");
} else {
fprintf(stderr, "renameover: a failed aside retry lost the old copy\n");
err++;
}
/* An unrelated file already sitting on the aside name must survive. */
ro_put(src, "new");
ro_put(aside, "mine");
if (!hts_rename_over_aside_selftest(opt, src, dst)) {
fprintf(stderr, "renameover: a taken aside name failed the move: %s\n",
strerror(errno));
err++;
} else if (!ro_is(dst, "new") || !ro_is(aside, "mine") ||
fexist_utf8(keep)) {
fprintf(stderr, "renameover: a taken aside name was not skipped\n");
err++;
}
(void) UNLINK(aside);
(void) UNLINK(keep);
/* A directory there reads as free to the probe, so the park must skip it
on the refusal rather than give up. */
ro_put(src, "new");
ro_put(dst, "old");
if (MKDIR(aside) == 0) {
if (!hts_rename_over_aside_selftest(opt, src, dst)) {
fprintf(stderr,
"renameover: a directory on the aside name blocked the "
"move: %s\n",
strerror(errno));
err++;
} else if (!ro_is(dst, "new") || fexist_utf8(keep)) {
fprintf(stderr, "renameover: a directory on the aside name was not "
"skipped\n");
err++;
}
(void) RMDIR(aside);
}
(void) UNLINK(keep);
}
(void) UNLINK(src);
(void) UNLINK(dst);
printf("renameover: %s\n", err ? "FAIL" : "OK");
return err;
@@ -6694,109 +6524,6 @@ static int st_threadwait(httrackp *opt, int argc, char **argv) {
return err;
}
/* #794: hts_gmtime() must own its output. The table is an independent oracle;
the threaded phase is what corrupts if it ever goes back to gmtime()'s
shared static. */
#define GMTIME_THREADS 8
#define GMTIME_ROUNDS 50000
static const struct {
time_t t;
int year, mon, mday, hour, min, sec, wday, yday;
} gmtime_refs[] = {
{(time_t) 0, 70, 0, 1, 0, 0, 0, 4, 0},
{(time_t) 951782400, 100, 1, 29, 0, 0, 0, 2, 59}, /* a leap day */
{(time_t) 1000000000, 101, 8, 9, 1, 46, 40, 0, 251},
{(time_t) 2147483647, 138, 0, 19, 3, 14, 7, 2, 18}, /* 32-bit ceiling */
};
#define GMTIME_REFS ((int) (sizeof(gmtime_refs) / sizeof(gmtime_refs[0])))
static hts_boolean gmtime_ref_matches(int i, const struct tm *tm) {
if (tm->tm_year != gmtime_refs[i].year || tm->tm_mon != gmtime_refs[i].mon ||
tm->tm_mday != gmtime_refs[i].mday ||
tm->tm_hour != gmtime_refs[i].hour || tm->tm_min != gmtime_refs[i].min ||
tm->tm_sec != gmtime_refs[i].sec || tm->tm_wday != gmtime_refs[i].wday ||
tm->tm_yday != gmtime_refs[i].yday)
return HTS_FALSE;
return HTS_TRUE;
}
static htsmutex gmtime_lock = HTSMUTEX_INIT;
static int gmtime_bad = 0;
static void gmtime_thread(void *arg) {
const int i = *(const int *) arg;
int bad = 0, round;
for (round = 0; round < GMTIME_ROUNDS; round++) {
struct tm tmv;
if (!hts_gmtime(gmtime_refs[i].t, &tmv) || !gmtime_ref_matches(i, &tmv))
bad++;
}
hts_mutexlock(&gmtime_lock);
gmtime_bad += bad;
hts_mutexrelease(&gmtime_lock);
}
static int st_gmtime(httrackp *opt, int argc, char **argv) {
static int idx[GMTIME_THREADS];
int err = 0, i;
(void) opt;
(void) argc;
(void) argv;
for (i = 0; i < GMTIME_REFS; i++) {
struct tm tmv;
if (!hts_gmtime(gmtime_refs[i].t, &tmv)) {
fprintf(stderr, "gmtime: conversion #%d failed\n", i);
err = 1;
} else if (!gmtime_ref_matches(i, &tmv)) {
fprintf(stderr,
"gmtime: #%d gave %04d-%02d-%02d %02d:%02d:%02d (wday %d, "
"yday %d)\n",
i, tmv.tm_year + 1900, tmv.tm_mon + 1, tmv.tm_mday, tmv.tm_hour,
tmv.tm_min, tmv.tm_sec, tmv.tm_wday, tmv.tm_yday);
err = 1;
}
}
/* the return is the only failure signal the callers have, so a helper that
always claims success leaves them formatting an uninitialised struct tm.
Out of range for a 64-bit time_t: NULL from gmtime_r, EINVAL from
_gmtime64_s. */
if (sizeof(time_t) >= 8) {
const time_t beyond = (time_t) INT64_MAX;
struct tm tmv;
if (hts_gmtime(beyond, &tmv)) {
fprintf(stderr,
"gmtime: an out-of-range time_t was reported converted\n");
err = 1;
}
}
for (i = 0; i < GMTIME_THREADS; i++) {
idx[i] = i % GMTIME_REFS;
if (hts_newthread(gmtime_thread, &idx[i]) != 0) {
fprintf(stderr, "gmtime: cannot spawn\n");
return 1;
}
}
htsthread_wait();
if (gmtime_bad != 0) {
fprintf(stderr, "gmtime: %d/%d concurrent conversions were corrupt\n",
gmtime_bad, GMTIME_THREADS * GMTIME_ROUNDS);
err = 1;
}
printf("gmtime self-test: %s\n", err ? "FAIL" : "OK");
return err;
}
#define CHANGES_RACE_FILES 8
#define CHANGES_RACE_ROUNDS 400
@@ -7063,8 +6790,6 @@ static const struct selftest_entry {
st_changes_race},
{"threadwait", "", "htsthread_wait() joins threads spawned just before it",
st_threadwait},
{"gmtime", "",
"hts_gmtime() fills the caller's buffer, not a static (#794)", st_gmtime},
{"backswap", "", "which backlog slots may be swapped to the ready table",
st_backswap},
{"pause", "", "randomized inter-file pause target self-test", st_pause},
@@ -7099,8 +6824,6 @@ static const struct selftest_entry {
st_growsize},
{"addlink", "", "htsAddLink codebase walk over an empty current path",
st_addlink},
{"logcallback", "", "log callback must not consume the log file's va_list",
st_logcallback},
{"cache", "<dir>", "cache read/write round-trip self-test", st_cache},
{"cacheindex", "", "cache-index (.ndx) parse must stay in bounds",
st_cacheindex},

View File

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

View File

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

View File

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

View File

@@ -1114,7 +1114,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(w->opt, tmppath, waczpath)) {
} else if (!hts_rename_over(tmppath, waczpath)) {
(void) UNLINK(fconv(catbuff, sizeof(catbuff), tmppath));
hts_log_print(w->opt, LOG_WARNING | LOG_ERRNO,
"WACZ: could not finalize %s", waczpath);
@@ -1392,8 +1392,16 @@ warc_writer *warc_open(httrackp *opt, const char *path) {
char ts[32];
time_t t = time(NULL);
struct tm tmv;
if (!hts_gmtime(t, &tmv))
#if defined(_WIN32)
struct tm *g = gmtime(&t);
if (g != NULL)
tmv = *g;
else
memset(&tmv, 0, sizeof(tmv));
#else
if (gmtime_r(&t, &tmv) == NULL)
memset(&tmv, 0, sizeof(tmv));
#endif
strftime(ts, sizeof(ts), "%Y%m%d%H%M%S", &tmv);
snprintf(catbuff, sizeof(catbuff), "httrack-%s.warc.gz", ts);
path =
@@ -1581,7 +1589,7 @@ static hts_boolean warc_commit(warc_writer *w) {
for (s = 0; s < nseg; s++) {
const char *final = warc_seg_path(w, s, finalbuf, sizeof(finalbuf));
snprintf(tmpbuf, sizeof(tmpbuf), "%s" WARC_TMP_SUFFIX, final);
if (!hts_rename_over(w->opt, tmpbuf, 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;

View File

@@ -520,13 +520,12 @@ static void proxytrack_add_DAV_Item(String * item, String * buff,
const char *filename, size_t size,
time_t timestamp, const char *mime,
int isDir, int isRoot, int isDefault) {
struct tm timetmbuf;
struct tm *timetm = &timetmbuf;
struct tm *timetm;
if (timestamp == (time_t) 0 || timestamp == (time_t) - 1) {
timestamp = time(NULL);
}
if (hts_gmtime(timestamp, timetm)) {
if ((timetm = gmtime(&timestamp)) != NULL) {
char tms[256 + 1];
const char *name;

View File

@@ -360,14 +360,21 @@ HTS_UNUSED static struct tm *convert_time_rfc822(struct tm *result, const char *
HTS_UNUSED static struct tm PT_GetTime(time_t t) {
struct tm tmbuf;
if (!hts_gmtime(t, &tmbuf)) {
#ifdef _WIN32
struct tm *tm = gmtime(&t);
#else
struct tm *tm = gmtime_r(&t, &tmbuf);
#endif
if (tm != NULL)
return *tm;
else {
/* an all-zero tm has tm_mday == 0, which the ARC date field prints as a
day of "00"; the epoch is the conventional "date unknown" */
memset(&tmbuf, 0, sizeof(tmbuf));
tmbuf.tm_year = 70;
tmbuf.tm_mday = 1;
return tmbuf;
}
return tmbuf;
}
HTS_UNUSED static int set_filetime(const char *file, struct tm *tm_time) {
struct utimbuf tim;

View File

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

View File

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

View File

@@ -3,14 +3,27 @@
set -euo pipefail
testdir=$(cd "$(dirname "$0")" && pwd)
# shellcheck source=tests/testlib.sh
. "${testdir}/testlib.sh"
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/httrack_changes_st.XXXXXX") || exit 1
trap 'set +e; rm -rf "$tmpdir"' EXIT
trap 'rm -rf "$tmpdir"' HUP INT QUIT PIPE TERM
# No pipe into grep: SIGPIPE would mask a failing exit status.
expect_ok() {
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
}
# --changes bucket accounting and JSON escaping (#714).
expect_ok "changes self-test" httrack -O "${tmpdir}/o1" -#test=changes run
# The report must hold against a transfer thread the crawl never joins.

View File

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

View File

@@ -9,5 +9,4 @@ set -euo pipefail
dir=$(mktemp -d)
trap 'set +e; rm -rf "$dir"' EXIT
out=$(httrack -O /dev/null -#test=cookieimport "$dir")
grep -q "cookieimport:.*OK" <<<"$out"
httrack -O /dev/null -#test=cookieimport "$dir" | grep -q "cookieimport:.*OK"

View File

@@ -8,5 +8,4 @@ set -euo pipefail
dir=$(mktemp -d)
trap 'set +e; rm -rf "$dir"' EXIT
out=$(httrack -O /dev/null -#test=direnum "$dir")
grep -q "direnum:.*OK" <<<"$out"
httrack -O /dev/null -#test=direnum "$dir" | grep -q "direnum:.*OK"

View File

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

View File

@@ -50,7 +50,7 @@ httrack "file://$deep/index.html" -O "$mir" -%F "$footer" -q -s0 -%v0 \
# The crawled page must exist (proves the URL wasn't rejected for length, so the
# footer path ran). Look under file/, not $mir, to skip the makeindex top index.
test -n "$(find "$mir/file" -name index.html)" || {
find "$mir/file" -name index.html | grep -q . || {
echo "page not mirrored; the oversized-footer path was not exercised" >&2
exit 1
}

View File

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

View File

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

View File

@@ -1,21 +0,0 @@
#!/bin/bash
#
set -euo pipefail
testdir=$(cd "$(dirname "$0")" && pwd)
# shellcheck source=tests/testlib.sh
. "${testdir}/testlib.sh"
# A non-UTC zone, so a helper that reached for localtime instead of gmtime gives
# a different answer: CI runners are UTC, where the two agree.
TZ=XXX5
export TZ
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/httrack_gmtime_st.XXXXXX") || exit 1
trap 'set +e; rm -rf "$tmpdir"' EXIT
trap 'rm -rf "$tmpdir"' HUP INT QUIT PIPE TERM
# Concurrent conversions do not tread on each other the way gmtime()'s shared
# static does (#794).
expect_ok "gmtime self-test" httrack -O "${tmpdir}/o1" -#test=gmtime

View File

@@ -19,7 +19,7 @@ printf -- '-*/zzmarker*\n' >"$tmp/rules.txt"
# the rules file lands in the URL/filter string, echoed back by the banner
run=$(httrack -O "$tmp/out" --quiet -n "-%S" "$tmp/rules.txt" \
"file://$tmp/index.html" 2>&1) || true
grep -q 'zzmarker' <<<"$run" || {
printf '%s\n' "$run" | grep -q 'zzmarker' || {
echo "FAIL: -%S rules file was not loaded"
printf '%s\n' "$run"
exit 1

View File

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

View File

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

View File

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

View File

@@ -8,5 +8,4 @@ set -euo pipefail
dir=$(mktemp -d)
trap 'set +e; rm -rf "$dir"' EXIT
out=$(httrack -O /dev/null -#test=longpath "$dir")
grep -q "longpath:.*OK" <<<"$out"
httrack -O /dev/null -#test=longpath "$dir" | grep -q "longpath:.*OK"

View File

@@ -9,5 +9,4 @@ set -euo pipefail
dir=$(mktemp -d)
trap 'set +e; rm -rf "$dir"' EXIT
out=$(httrack -O /dev/null -#test=mirrorio "$dir")
grep -q "mirrorio:.*OK" <<<"$out"
httrack -O /dev/null -#test=mirrorio "$dir" | grep -q "mirrorio:.*OK"

View File

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

View File

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

View File

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

View File

@@ -7,11 +7,11 @@ set -eu
# Bare -#test lists known tests (printed to stderr).
list=$(httrack -#test 2>&1)
grep -q "filter" <<<"$list" || exit 1
grep -q "cache-writefail" <<<"$list" || exit 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
grep -q "Unknown self-test" <<<"$err" || exit 1
printf '%s\n' "$err" | grep -q "Unknown self-test" || exit 1

View File

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

View File

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

View File

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

View File

@@ -3,14 +3,27 @@
set -euo pipefail
testdir=$(cd "$(dirname "$0")" && pwd)
# shellcheck source=tests/testlib.sh
. "${testdir}/testlib.sh"
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/httrack_threadwait_st.XXXXXX") || exit 1
trap 'set +e; rm -rf "$tmpdir"' EXIT
trap 'rm -rf "$tmpdir"' HUP INT QUIT PIPE TERM
# No pipe into grep: SIGPIPE would mask a failing exit status.
expect_ok() {
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

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,8 @@
#!/bin/bash
#
# Stick to POSIX tool flags: macOS ships BSD grep/sed, not the GNU ones.
# 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 policy (-#test=cache-writefail <dir>). #174/#219: disk
# full or a failure streak aborts cleanly; an isolated failure or an oversized
@@ -15,13 +17,13 @@ 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.
grep -qx "cache-writefail: OK" <<<"$out" || {
printf '%s\n' "$out" | grep -qx "cache-writefail: OK" || {
echo "expected 'cache-writefail: OK', got: $out" >&2
exit 1
}
# A skipped entry must be warned about with its URL.
grep -q "entry not cached: example.com/" <<<"$out" || {
printf '%s\n' "$out" | grep -q "entry not cached: example.com/" || {
echo "expected a URL-bearing skip warning" >&2
exit 1
}

View File

@@ -1,6 +1,8 @@
#!/bin/bash
#
# Stick to POSIX tool flags: macOS ships BSD grep/sed, not the GNU ones.
# 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.
# unzRepair header read must not overflow a signed shift (-#test=zip-repair-shift
# <dir>). A damaged local file header whose CRC high word has bit 15 set made
@@ -13,7 +15,7 @@ trap 'set +e; rm -rf "$dir"' EXIT
out=$(httrack -#test=zip-repair-shift "$dir")
grep -qx "zip-repair-shift: OK (recovered 1 entry)" <<<"$out" || {
printf '%s\n' "$out" | grep -qx "zip-repair-shift: OK (recovered 1 entry)" || {
echo "expected 'zip-repair-shift: OK (recovered 1 entry)', got: $out" >&2
exit 1
}

View File

@@ -10,8 +10,7 @@ set -euo pipefail
httrack_bin=$(cd "$(dirname "$(command -v httrack)")" && pwd)/httrack
registry=$("$httrack_bin" -#test 2>&1 || true)
if ! grep -q '^ warc-wacz' <<<"$registry"; then
if ! "$httrack_bin" -#test 2>&1 | grep -q '^ warc-wacz'; then
echo "warc-wacz self-test unavailable (build without OpenSSL); skipping"
exit 77
fi

View File

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

View File

@@ -1,98 +0,0 @@
#!/bin/bash
#
# A completed FTP transfer used to end as a 0-byte file when its ready backlog
# slot was swapped to the on-disk table (#797): the FTP worker writes url_sav
# itself and leaves no in-memory body, and the swap round-trip zeroed the slot's
# size, so the link writer then created an empty file over the bytes.
set -euo pipefail
: "${top_srcdir:=..}"
testdir=$(cd "$(dirname "$0")" && pwd)
# shellcheck source=tests/testlib.sh
. "${testdir}/testlib.sh"
python=$(find_python) || ! echo "python3 not found; skipping" >&2 || exit 77
command -v httrack >/dev/null || {
echo "could not find httrack" >&2
exit 1
}
server=$(nativepath "${testdir}/ftp-server.py")
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/httrack_ftp.XXXXXX")
serverpid=
cleanup() {
stop_server "$serverpid"
rm -rf "$tmpdir"
}
trap 'set +e; cleanup' EXIT
trap cleanup HUP INT QUIT PIPE TERM
root="${tmpdir}/root"
out="${tmpdir}/crawl"
mkdir -p "$root" "$out"
fail() {
echo "FAIL: $*" >&2
exit 1
}
ok() { echo "OK: $*"; }
size_of() { wc -c <"$1" | tr -d '[:space:]'; }
# Distinct bodies and lengths, so a mirror that mixed two of them up cannot
# compare equal.
names=""
for i in 1 2 3 4 5 6 7 8 9 10 11 12; do
names="${names} f${i}"
"$python" -c 'import sys; sys.stdout.buffer.write(
("BODY-%s " % sys.argv[1]).encode() + sys.argv[1].encode() * (400 * int(sys.argv[1])))' \
"$i" >"${root}/f${i}.bin"
done
serverlog="${tmpdir}/server.out"
"$python" "$server" --root "$(nativepath "$root")" >"$serverlog" 2>&1 &
serverpid=$!
port=
for _ in $(seq 1 300); do
line=$(grep -m1 '^PORT ' "$serverlog" 2>/dev/null) && port="${line#PORT }" && break
kill -0 "$serverpid" 2>/dev/null || {
echo "ftp server exited early: $(cat "$serverlog")" >&2
exit 1
}
sleep 0.1
done
test -n "$port" || {
echo "could not discover ftp server port: $(cat "$serverlog")" >&2
exit 1
}
host="127.0.0.1_${port}"
urls=()
for name in $names; do
urls+=("ftp://127.0.0.1:${port}/${name}.bin")
done
# -c8, never -c1: a slot is only swapped out while other transfers keep the
# parser from picking it up. -Z because the swap only logs at debug level, and
# the run below proves nothing unless it happened. Bounded like every crawl
# pass: a wedge must fail the test, not hang the job until CI discards the log.
run_with_timeout 300 httrack "${urls[@]}" -O "$out" --quiet -Z -c8 \
--disable-security-limits --robots=0 --timeout=20 --max-time=120 --retries=1 \
>"${tmpdir}/log" 2>&1
swapped=$(grep -ac 'slots ready moved to background' "${out}/hts-log.txt" || true)
test "${swapped:-0}" -gt 0 ||
fail "no ready slot was swapped to disk; this run never reached the bug"
ok "the crawl swapped ready slots to the on-disk table ${swapped} times"
for name in $names; do
test -f "${out}/${host}/${name}.bin" || fail "${name}.bin was not mirrored"
cmp -s "${out}/${host}/${name}.bin" "${root}/${name}.bin" ||
fail "${name}.bin has $(size_of "${out}/${host}/${name}.bin") of the" \
"$(size_of "${root}/${name}.bin") bytes served"
done
ok "every FTP transfer landed on disk intact"
errors=$(grep -a 'Error:' "${out}/hts-log.txt" || true)
test -z "$errors" || fail "the crawl reported errors: ${errors}"
ok "no transfer was reported failed"

View File

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

View File

@@ -1,108 +0,0 @@
#!/bin/bash
#
# Issue #782: a 304 during --update overwrote the whole response struct with the
# cache entry, dropping the 8 KB header buffer it owned. Only a leak checker
# sees it and the sanitized CI leg runs with detect_leaks off, so this one turns
# it back on around a two-pass mini304 crawl; the fresh pass is the control.
set -eu
: "${top_srcdir:=..}"
testdir=$(cd "$(dirname "$0")" && pwd)
# shellcheck source=tests/testlib.sh
. "${testdir}/testlib.sh"
server=$(nativepath "${testdir}/local-server.py")
root=$(nativepath "${testdir}/server-root")
python=$(find_python) || ! echo "python3 not found; skipping" >&2 || exit 77
probe=$(ASAN_OPTIONS=help=1 httrack -O /dev/null --version 2>&1 || true)
case "$probe" in
*AddressSanitizer*) ;;
*)
echo "not a sanitized build, skipping" >&2
exit 77
;;
esac
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/httrack_304leak.XXXXXX") || exit 1
serverpid=
cleanup() {
stop_server "$serverpid"
rm -rf "$tmpdir"
}
trap 'set +e; cleanup' EXIT
trap cleanup HUP INT QUIT PIPE TERM
serverlog="${tmpdir}/server.log"
: >"$serverlog"
"$python" "$server" --root "$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
}
out="${tmpdir}/crawl"
mkdir "$out"
url="http://127.0.0.1:${port}/mini304/index.html"
# Last value wins in the sanitizer flag parser, so this overrides the job's
# settings; the report file, not the exit status, is the verdict.
asan_base="${ASAN_OPTIONS:+$ASAN_OPTIONS:}detect_leaks=1:abort_on_error=0:exitcode=0"
# run_pass <label> <extra httrack args...>
run_pass() {
local label="$1"
shift
rm -f "${tmpdir}/asan".*
printf '[%s] ..\t' "$label"
ASAN_OPTIONS="${asan_base}:log_path=${tmpdir}/asan" \
httrack -O "$out" --quiet --disable-security-limits --robots=0 \
--timeout=30 --retries=1 -c1 "$@" "$url" >"${tmpdir}/log_${label}" 2>&1 || true
report=$(cat "${tmpdir}/asan".* 2>/dev/null || true)
test -z "$report" || {
echo "FAIL: sanitizer report on the ${label} pass"
echo "$report"
exit 1
}
echo "OK (clean)"
}
run_pass fresh
run_pass update --update
# only --warc-file stashes the two header blocks, so the archive limb of the fix
# runs on this pass alone
run_pass warc --update --warc-file leak
# Teeth: the update passes must have gone through the 304 path, or a run that
# re-downloaded everything would report clean having tested nothing.
printf '[update used the cache] ..\t'
if grep -aqE "no files updated" "${out}/hts-log.txt"; then
echo "OK"
else
echo "FAIL: the update pass did not report unchanged files"
exit 1
fi
# ... and the WARC pass must have written an archive, or it stashed nothing.
printf '[warc pass archived] ..\t'
if [ -n "$(find "$out" -name 'leak*.warc*' -size +0c 2>/dev/null)" ]; then
echo "OK"
else
echo "FAIL: the --warc-file pass produced no archive"
exit 1
fi

View File

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

View File

@@ -9,5 +9,4 @@ set -euo pipefail
# (#614) was stripped as if it were the default. The default is scheme-aware
# (#638): an explicit :80 on https/ftp stays, :443/:21 strip under their own
# scheme. All assertions live in the engine self-test.
out=$(httrack -O /dev/null -#test=stripport)
grep -q "stripport self-test OK" <<<"$out"
httrack -O /dev/null -#test=stripport | grep -q "stripport self-test OK"

View File

@@ -21,7 +21,7 @@ test "$rc" -ne 77 || {
echo "SKIP: local crawl prerequisites missing" >&2
exit 77
}
if grep -q "could not discover server port" <<<"$out"; then
if printf '%s\n' "$out" | grep -q "could not discover server port"; then
echo "SKIP: server port announce raced" >&2
exit 77
fi
@@ -30,7 +30,7 @@ fi
# together they pin the fast exit on the 3s watchdog, not httrack giving up.
test "$rc" -ne 0 || fail "stalled crawl reported success"
test "$elapsed" -lt 25 || fail "watchdog fired late (${elapsed}s)"
grep -q "watchdog fired" <<<"$out" ||
printf '%s\n' "$out" | grep -q "watchdog fired" ||
fail "crawl failed but not via the watchdog: $out"
echo "crawl watchdog OK (reaped in ${elapsed}s)"

View File

@@ -71,8 +71,7 @@ grep -qE 'st_strsafe|strcpy_safe_' "$oracle" || {
# The payload. Dropping the raw frames first is what makes it specific: both
# names are static, so .dynsym could never have carried them. Not anchored on
# the "0xOFF:" line, the inline chain puts the name on either half.
symbolized=$(grep -v "$rawframe" "$out" || true)
grep -qE 'st_strsafe|strcpy_safe_' <<<"$symbolized" || {
grep -v "$rawframe" "$out" | grep -qE 'st_strsafe|strcpy_safe_' || {
echo "the handler did not name the hidden frames:" >&2
cat "$out" >&2
exit 1

View File

@@ -169,8 +169,7 @@ test "${written}" -lt "${#profile}" ||
fail "the file-size limit did not stop the write (${written} bytes landed)"
check_clipped "Unable to write ${#profile} bytes in the the init file " "${path}/p"
reply=$(get "${port}" /server/index.html || true)
grep -q '200 OK' <<<"$reply" ||
get "${port}" /server/index.html | grep -q '200 OK' ||
fail "the server stopped answering after the refused saves"
echo "PASS"

View File

@@ -64,7 +64,6 @@ TESTS = \
01_engine-filterdual.test \
01_engine-ftp-line.test \
01_engine-ftp-userpass.test \
01_engine-gmtime.test \
01_engine-backswap.test \
01_engine-cacheindex.test \
01_engine-hashtable.test \
@@ -77,7 +76,6 @@ TESTS = \
01_engine-escape-room.test \
01_engine-inplace-escape.test \
01_engine-lastchar.test \
01_engine-logcallback.test \
01_engine-makeindex.test \
01_engine-mime.test \
01_engine-parse.test \
@@ -227,9 +225,6 @@ TESTS = \
102_local-ftp-refetch.test \
103_teardown-status.test \
104_engine-warc-longurl.test \
105_suite-timeout.test \
110_local-ftp-parallel.test \
111_local-ftp-update-rest.test \
114_local-update-304-leak.test
105_suite-timeout.test
CLEANFILES = check-network_sh.cache

View File

@@ -1,9 +1,7 @@
/* Borrows Windows' rename() for 01_engine-renameover.test, since POSIX cannot
reach hts_rename_over()'s aside fallback: an existing target is refused with
reach hts_rename_over()'s unlink fallback: an existing target is refused with
EEXIST, and RENAMEFAIL_MODE=locked reports EACCES instead, as the CRT does
for a source another process holds. RENAMEFAIL_ASIDE_FAILS=N refuses the
first N moves back out of the parked ".hts-old" name, which is the only way
to reach hts_rename_over()'s restore retry (#790). */
for a source another process holds. */
#define _GNU_SOURCE
#include <dlfcn.h>
@@ -20,9 +18,7 @@ SHIM_EXPORT int rename(const char *oldpath, const char *newpath);
SHIM_EXPORT int rename(const char *oldpath, const char *newpath) {
static int (*real_rename)(const char *, const char *) = NULL;
static int aside_failures = 0;
const char *const mode = getenv("RENAMEFAIL_MODE");
const char *const aside_fails = getenv("RENAMEFAIL_ASIDE_FAILS");
const int locked = mode != NULL && strcmp(mode, "locked") == 0;
struct stat st;
@@ -30,13 +26,7 @@ SHIM_EXPORT int rename(const char *oldpath, const char *newpath) {
errno = EACCES;
return -1;
}
if (aside_fails != NULL && strstr(oldpath, ".hts-old") != NULL &&
aside_failures < atoi(aside_fails)) {
aside_failures++;
errno = EACCES;
return -1;
}
if (aside_fails == NULL && stat(newpath, &st) == 0) {
if (stat(newpath, &st) == 0) {
errno = EEXIST;
return -1;
}

View File

@@ -24,24 +24,6 @@ nativepath() {
fi
}
# Run an engine self-test and require its "<label>: OK" line. 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
}
is_windows() {
case "$(uname -s)" in
MINGW* | MSYS* | CYGWIN*) return 0 ;;

View File

@@ -53,13 +53,13 @@ echo "stub browser invoked with: \$1" >&2
opturl="\${1%/}/server/option2.html"
warcurl="\${1%/}/server/option9.html"
smurl="\${1%/}/server/option8.html"
if body="\$(curl -fsSL --max-time 20 "\$1")" && grep -qai httrack <<<"\$body" && grep -qaF step2.html <<<"\$body" &&
opt="\$(curl -fsSL --max-time 20 "\$opturl")" && grep -qaF "title='" <<<"\$opt" &&
grep -qaF 'name="singlefile"' <<<"\$opt" && grep -qaF 'name="singlefilemax"' <<<"\$opt" &&
! grep -qaF '\${LANG_SINGLEFILE}' <<<"\$opt" &&
warc="\$(curl -fsSL --max-time 20 "\$warcurl")" && grep -qaF 'name="warcfile"' <<<"\$warc" && grep -qaF WARC <<<"\$warc" &&
grep -qaF 'name="changes"' <<<"\$warc" && grep -qaF hts-changes.json <<<"\$warc" &&
sm="\$(curl -fsSL --max-time 20 "\$smurl")" && grep -qaF 'name="sitemapurl"' <<<"\$sm" && grep -qaF 'name="sitemap"' <<<"\$sm"; then
if body="\$(curl -fsSL --max-time 20 "\$1")" && printf '%s' "\$body" | grep -qai httrack && printf '%s' "\$body" | grep -qaF step2.html &&
opt="\$(curl -fsSL --max-time 20 "\$opturl")" && printf '%s' "\$opt" | grep -qaF "title='" &&
printf '%s' "\$opt" | grep -qaF 'name="singlefile"' && printf '%s' "\$opt" | grep -qaF 'name="singlefilemax"' &&
! printf '%s' "\$opt" | grep -qaF '\${LANG_SINGLEFILE}' &&
warc="\$(curl -fsSL --max-time 20 "\$warcurl")" && printf '%s' "\$warc" | grep -qaF 'name="warcfile"' && printf '%s' "\$warc" | grep -qaF WARC &&
printf '%s' "\$warc" | grep -qaF 'name="changes"' && printf '%s' "\$warc" | grep -qaF hts-changes.json &&
sm="\$(curl -fsSL --max-time 20 "\$smurl")" && printf '%s' "\$sm" | grep -qaF 'name="sitemapurl"' && printf '%s' "\$sm" | grep -qaF 'name="sitemap"'; then
echo PASS >"$marker"
else
echo "FAIL: unexpected response from \$1" >"$marker"