mirror of
https://github.com/xroche/httrack.git
synced 2026-07-27 19:12:54 +03:00
Compare commits
12 Commits
fix/strlen
...
fix/proxyt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7d45027a8 | ||
|
|
8c5b208fac | ||
|
|
b7cff7aeee | ||
|
|
bc8b6cfa00 | ||
|
|
260cef25fe | ||
|
|
bd60dac914 | ||
|
|
0984aa2530 | ||
|
|
13953867e8 | ||
|
|
bc6c53598c | ||
|
|
809d5b6ffc | ||
|
|
db89c82d79 | ||
|
|
ad3f051ac6 |
9
.github/workflows/windows-build.yml
vendored
9
.github/workflows/windows-build.yml
vendored
@@ -211,7 +211,7 @@ jobs:
|
||||
pass=0 fail=0 skip=0 failed="" skipped="" deadline=0
|
||||
for t in 00_runnable.test 01_engine-*.test 01_zlib-*.test \
|
||||
*_local-*.test 13_crawl_proxy_https.test 58_watchdog.test \
|
||||
60_crawl-log-salvage.test 116_engine-rtrim.test; do
|
||||
60_crawl-log-salvage.test; do
|
||||
elapsed=$((SECONDS - started))
|
||||
if [ "$elapsed" -ge "$suite_deadline" ]; then
|
||||
echo "::error::suite deadline: ${elapsed}s elapsed, stopping before $t"
|
||||
@@ -260,10 +260,11 @@ jobs:
|
||||
# tested nothing: pin the skips, and floor the passes in case the glob empties.
|
||||
# footer-overflow and purge-longpath skip on Windows (need a path past MAX_PATH);
|
||||
# crange pending #581;
|
||||
# webdav-mime needs a reapable background listener, which MSYS cannot give it;
|
||||
# webdav-default and webdav-mime need a reapable background listener, which MSYS cannot give them;
|
||||
# badmtime needs a filesystem that stores an mtime past gmtime's range;
|
||||
# single-file ends on a GUI half needing htsserver, which this job does not build.
|
||||
expected_skips=" 01_engine-footer-overflow.test 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"
|
||||
# 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 120_local-proxytrack-webdav-default.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; }
|
||||
|
||||
14
AGENTS.md
14
AGENTS.md
@@ -30,12 +30,14 @@ 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 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.
|
||||
- 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"`.
|
||||
|
||||
## Hard invariants
|
||||
- **Generated autotools files are NOT in git.** `configure`, every
|
||||
|
||||
@@ -237,7 +237,7 @@ Build options:
|
||||
x replace external html links by error pages (--replace-external)
|
||||
%x do not include any password for external password protected websites (%x0 include) (--no-passwords)
|
||||
%q *include query string for local files (useless, for information purpose only) (%q0 don't include) (--include-query-string)
|
||||
o *generate output html file in case of error (404..) (o0 don't generate) (--generate-errors)
|
||||
o *save the server's error pages (404..) (o0 discard them) (--generate-errors)
|
||||
X *purge old files after update (X0 keep delete) (--purge-old[=N])
|
||||
|
||||
Spider options:
|
||||
@@ -413,7 +413,7 @@ site. Specifically, the defauls are:
|
||||
NN name conversion type (0 *original structure, 1+: see below)
|
||||
LN long names (L1 *long names / L0 8-3 conversion)
|
||||
K keep original links (e.g. http://www.adr/link) (K0 *relative link)
|
||||
o *generate output html file in case of error (404..) (o0 don't generate)
|
||||
o *save the server's error pages (404..) (o0 discard them)
|
||||
X *purge old files after update (X0 keep delete)
|
||||
bN accept cookies in cookies.txt (0=do not accept,* 1=accept)
|
||||
u check document type if unknown (cgi,asp..) (u0 don't check, * u1 check but /, u2 check always)
|
||||
@@ -473,11 +473,11 @@ store them with the same names used on the web site.
|
||||
URLs within this web site are adjusted to point to the files in the
|
||||
mirror.
|
||||
|
||||
<pre><b><i> o *generate output html file in case of error (404..) (o0 don't generate) </i></b></pre>
|
||||
<pre><b><i> o *save the server's error pages (404..) (o0 discard them) </i></b></pre>
|
||||
|
||||
<p align=justify> IF there are errors in downloading, create a file that
|
||||
indicates that the URL was not found. This makes browsing go a lot
|
||||
smoother.
|
||||
<p align=justify> IF a page cannot be downloaded, the error page the
|
||||
server sent is saved in its place, so a broken link still lands on the
|
||||
site's own 'not found' page. This makes browsing go a lot smoother.
|
||||
|
||||
<pre><b><i> X *purge old files after update (X0 keep delete) </i></b></pre>
|
||||
|
||||
@@ -1011,7 +1011,7 @@ Build options:
|
||||
LN long names (L1 *long names / L0 8-3 conversion)
|
||||
K keep original links (e.g. http://www.adr/link) (K0 *relative link)
|
||||
x replace external html links by error pages
|
||||
o *generate output html file in case of error (404..) (o0 don't generate)
|
||||
o *save the server's error pages (404..) (o0 discard them)
|
||||
X *purge old files after update (X0 keep delete)
|
||||
%x do not include any password for external password protected websites (%x0 include) (--no-passwords)
|
||||
%q *include query string for local files (information only) (%q0 don't include) (--include-query-string)
|
||||
@@ -1118,9 +1118,9 @@ deactivated byt his process.
|
||||
httrack http://www.shoesizes.com -O /tmp/shoesizes -x
|
||||
</i></b></pre>
|
||||
|
||||
<p align=justify> This option prevents the generation of '404' error
|
||||
files to replace files that were not found even though there were URLs
|
||||
pointing to them. It is useful for saving space as well as eliminating
|
||||
<p align=justify> This option keeps the server's '404' error pages out
|
||||
of the mirror, even though there were URLs pointing to the missing
|
||||
files. It is useful for saving space as well as eliminating
|
||||
unnecessary files in operations where a working web site is not the
|
||||
desired result.
|
||||
|
||||
|
||||
@@ -777,8 +777,8 @@ information purpose only) (%q0 don’t include)
|
||||
<td width="82%">
|
||||
|
||||
|
||||
<p>*generate output html file in case of error (404..) (o0
|
||||
don’t generate) (--generate-errors)</p></td></tr>
|
||||
<p>*save the server’s error pages (404..) (o0 discard
|
||||
them) (--generate-errors)</p></td></tr>
|
||||
<tr valign="top" align="left">
|
||||
<td width="9%"></td>
|
||||
<td width="4%">
|
||||
|
||||
@@ -122,7 +122,7 @@ h4 { margin: 0; font-weight: bold; font-size: 1.18em; }
|
||||
</small><br><br>
|
||||
<!-- -->
|
||||
<li>No error pages</li>
|
||||
<br><small>Do not generate error pages (if a 404 error occurred, for example)
|
||||
<br><small>Do not save the error pages sent by the server (if a 404 error occurred, for example)
|
||||
<br>If a page is missing on the remote site, there will not be any warning on the local site
|
||||
</small><br><br>
|
||||
<!-- -->
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
.\"
|
||||
.\" This file is generated by man/makeman.sh; do not edit by hand.
|
||||
.\" SPDX-License-Identifier: GPL-3.0-or-later
|
||||
.TH httrack 1 "26 July 2026" "httrack website copier"
|
||||
.TH httrack 1 "27 July 2026" "httrack website copier"
|
||||
.SH NAME
|
||||
httrack \- offline browser : copy websites to a local directory
|
||||
.SH SYNOPSIS
|
||||
@@ -220,7 +220,7 @@ do not include any password for external password protected websites (%x0 includ
|
||||
.IP \-%g
|
||||
strip query keys for dedup ([host/pattern=]key1,key2,...) (\-\-strip\-query <param>)
|
||||
.IP \-o
|
||||
*generate output html file in case of error (404..) (o0 don't generate) (\-\-generate\-errors)
|
||||
*save the server's error pages (404..) (o0 discard them) (\-\-generate\-errors)
|
||||
.IP \-X
|
||||
*purge old files after update (X0 keep delete) (\-\-purge\-old[=N])
|
||||
.IP \-%p
|
||||
|
||||
@@ -501,11 +501,13 @@ int optinclude_file(const char *name, int *argc, char **argv, char *x_argvblk,
|
||||
/* read line */
|
||||
linput(fp, line, 250);
|
||||
hts_lowcase(line);
|
||||
/* trim first: a blank line is skipped, not parsed as an option */
|
||||
hts_rtrim(line, HTS_REALSPACES);
|
||||
if (strnotempty(line)) {
|
||||
/* no comment line: # // ; */
|
||||
if (strchr("#/;", line[0]) == NULL) {
|
||||
/* right trim */
|
||||
a = line + strlen(line) - 1;
|
||||
while(is_realspace(*a))
|
||||
*(a--) = '\0';
|
||||
/* jump "set " and spaces */
|
||||
a = line;
|
||||
while(is_realspace(*a))
|
||||
@@ -575,6 +577,7 @@ int optinclude_file(const char *name, int *argc, char **argv, char *x_argvblk,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
fclose(fp);
|
||||
|
||||
@@ -391,6 +391,56 @@ 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, ©) != 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;
|
||||
}
|
||||
@@ -646,7 +696,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(back->url_sav, back->tmpfile);
|
||||
saved = hts_rename_over(opt, back->url_sav, back->tmpfile);
|
||||
}
|
||||
if (!saved) {
|
||||
hts_log_print(opt, LOG_WARNING | LOG_ERRNO,
|
||||
@@ -687,7 +737,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 (!hts_rename_over(opt, 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);
|
||||
@@ -818,7 +868,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 (hts_rename_over(opt, unpacked, back[p].url_sav)) {
|
||||
/* The temp bypassed filecreate(), which is what chmods. */
|
||||
#ifndef _WIN32
|
||||
chmod(back[p].url_sav, HTS_ACCESS_FILE);
|
||||
@@ -1173,6 +1223,13 @@ 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));
|
||||
@@ -1266,7 +1323,10 @@ 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) {
|
||||
(*dst)->r.size = size;
|
||||
/* 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.headers = NULL;
|
||||
if (back_string_unserialize(fp, &(*dst)->r.headers) == 0)
|
||||
return 0; /* ok */
|
||||
@@ -1705,10 +1765,7 @@ int back_clear_entry(lien_back * back) {
|
||||
back->r.soc = INVALID_SOCKET;
|
||||
}
|
||||
|
||||
if (back->r.adr != NULL) { // reste un bloc à désallouer
|
||||
freet(back->r.adr);
|
||||
back->r.adr = NULL;
|
||||
}
|
||||
back_free_response(&back->r);
|
||||
if (back->chunk_adr != NULL) { // reste un bloc à désallouer
|
||||
freet(back->chunk_adr);
|
||||
back->chunk_adr = NULL;
|
||||
@@ -1721,12 +1778,6 @@ 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;
|
||||
@@ -4031,6 +4082,9 @@ 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);
|
||||
|
||||
@@ -491,7 +491,7 @@ char *bauth_prefix(char *prefix, const char *adr, const char *fil) {
|
||||
if (a)
|
||||
*a = '\0';
|
||||
if (strchr(prefix, '/')) {
|
||||
a = hts_lastcharptr(prefix);
|
||||
a = prefix + strlen(prefix) - 1;
|
||||
while(*a != '/')
|
||||
a--;
|
||||
*(a + 1) = '\0';
|
||||
|
||||
@@ -985,6 +985,7 @@ 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),
|
||||
|
||||
@@ -3774,7 +3774,8 @@ int htsAddLink(htsmoduleStruct * str, char *link) {
|
||||
strcpybuff(codebase, heap(ptr)->fil);
|
||||
else
|
||||
strcpybuff(codebase, heap(heap(ptr)->precedent)->fil);
|
||||
a = hts_lastcharptr(codebase);
|
||||
// empty codebase has no last char; codebase-1 would underflow
|
||||
a = codebase[0] != '\0' ? codebase + strlen(codebase) - 1 : codebase;
|
||||
while((*a) && (*a != '/') && (a > codebase))
|
||||
a--;
|
||||
if (*a == '/')
|
||||
|
||||
16
src/htsftp.c
16
src/htsftp.c
@@ -483,16 +483,19 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
|
||||
back->r.totalsize = size;
|
||||
}
|
||||
}
|
||||
// REST?
|
||||
if (fexist(back->url_sav) && (transfer_list == 0)) {
|
||||
/* 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)) {
|
||||
strcpybuff(back->info, "rest");
|
||||
snprintf(line, sizeof(line), "REST " LLintP, (LLint) fsize(back->url_sav));
|
||||
snprintf(line, sizeof(line), "REST " LLintP,
|
||||
(LLint) back->range_req_size);
|
||||
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;
|
||||
} // sinon tant pis
|
||||
} // else never mind
|
||||
}
|
||||
} // sinon tant pis
|
||||
}
|
||||
@@ -617,9 +620,12 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
|
||||
// Ok, connexion initiée
|
||||
//
|
||||
if (soc_dat != INVALID_SOCKET) {
|
||||
if (rest_understood) { // REST envoyée et comprise
|
||||
if (rest_understood) { // REST sent and understood
|
||||
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,
|
||||
|
||||
@@ -558,8 +558,7 @@ void help(const char *app, int more) {
|
||||
infomsg
|
||||
(" %q *include query string for local files (useless, for information purpose only) (%q0 don't include)");
|
||||
infomsg(" %g strip query keys for dedup ([host/pattern=]key1,key2,...)");
|
||||
infomsg
|
||||
(" o *generate output html file in case of error (404..) (o0 don't generate)");
|
||||
infomsg(" o *save the server's error pages (404..) (o0 discard them)");
|
||||
infomsg(" X *purge old files after update (X0 keep delete)");
|
||||
infomsg(" %p preserve html files 'as is' (identical to '-K4 -%F \"\"')");
|
||||
infomsg(" %T links conversion to UTF-8");
|
||||
|
||||
33
src/htslib.c
33
src/htslib.c
@@ -1322,7 +1322,7 @@ void treathead(t_cookie * cookie, const char *adr, const char *fil, htsblk * ret
|
||||
p++; // sauter espaces
|
||||
if ((int) strlen(rcvd + p) < 250) { // pas trop long?
|
||||
char tmp[256];
|
||||
char *a = NULL;
|
||||
char *a = NULL, *b = NULL;
|
||||
|
||||
strcpybuff(tmp, rcvd + p);
|
||||
a = strstr(tmp, "filename=");
|
||||
@@ -1335,9 +1335,15 @@ void treathead(t_cookie * cookie, const char *adr, const char *fil, htsblk * ret
|
||||
|
||||
while((c = strchr(a, '/'))) /* skip all / (see RFC2616) */
|
||||
a = c + 1;
|
||||
hts_rtrim(a, HTS_SPACES);
|
||||
if ((int) strlen(a) < 200) { // pas trop long?
|
||||
strcpybuff(retour->cdispo, a);
|
||||
b = a + strlen(a) - 1;
|
||||
while(is_space(*b))
|
||||
b--;
|
||||
b++;
|
||||
if (b) {
|
||||
*b = '\0';
|
||||
if ((int) strlen(a) < 200) { // pas trop long?
|
||||
strcpybuff(retour->cdispo, a);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2697,17 +2703,8 @@ void hts_now_iso8601(char out[32]) {
|
||||
time_t t = time(NULL);
|
||||
struct tm tmv;
|
||||
|
||||
#if defined(_WIN32)
|
||||
struct tm *g = gmtime(&t);
|
||||
|
||||
if (g != NULL)
|
||||
tmv = *g;
|
||||
else
|
||||
if (!hts_gmtime(t, &tmv))
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -3309,9 +3306,8 @@ int ishtml(httrackp * opt, const char *fil) {
|
||||
}
|
||||
|
||||
/* Search for known ext */
|
||||
for (a = hts_lastcharptr(fil_noquery);
|
||||
*a != '.' && *a != '/' && a > fil_noquery; a--)
|
||||
;
|
||||
for(a = fil_noquery + strlen(fil_noquery) - 1;
|
||||
*a != '.' && *a != '/' && a > fil_noquery; a--) ;
|
||||
if (*a == '.') { // a une extension
|
||||
char BIGSTK fil_noquery[HTS_URLMAXSIZE * 2];
|
||||
char *b;
|
||||
@@ -4248,8 +4244,9 @@ HTSEXT_API hts_boolean get_httptype_sized(httrackp *opt, char *s, size_t ssize,
|
||||
return 1;
|
||||
} else {
|
||||
/* Check html -> text/html */
|
||||
const char *a = hts_lastcharptr(fil);
|
||||
const char *a = fil + strlen(fil) - 1;
|
||||
|
||||
/* a < fil when fil is empty: bound before dereferencing */
|
||||
while ((a > fil) && (*a != '.') && (*a != '/'))
|
||||
a--;
|
||||
if (a >= fil && *a == '.' && strlen(a) < 32) {
|
||||
|
||||
13
src/htslib.h
13
src/htslib.h
@@ -157,6 +157,19 @@ 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
|
||||
|
||||
|
||||
@@ -823,7 +823,7 @@ int url_savename(lien_adrfilsave *const afs,
|
||||
// Change the extension? e.g. php3 saved as html, cgi as html or gif/xbm
|
||||
// depending on the resolved type.
|
||||
if (ext_chg && !opt->no_type_change) {
|
||||
char *a = hts_lastcharptr(fil);
|
||||
char *a = fil + strlen(fil) - 1;
|
||||
|
||||
if ((opt->debug > 1) && (opt->log != NULL)) {
|
||||
if (ext_chg == 1)
|
||||
@@ -858,7 +858,7 @@ int url_savename(lien_adrfilsave *const afs,
|
||||
}
|
||||
// Rechercher premier / et dernier .
|
||||
{
|
||||
const char *a = hts_lastcharptr(fil);
|
||||
const char *a = fil + strlen(fil) - 1;
|
||||
|
||||
// passer structures
|
||||
start_pos = fil;
|
||||
@@ -1203,29 +1203,29 @@ int url_savename(lien_adrfilsave *const afs,
|
||||
switch (opt->savename_type % 100) {
|
||||
case 4:
|
||||
case 5:{ // séparer par types
|
||||
const char *a = hts_lastcharptr(fil);
|
||||
const char *a = fil + strlen(fil) - 1;
|
||||
|
||||
// passer structures
|
||||
while ((a > fil) && (*a != '/') && (*a != '\\'))
|
||||
a--;
|
||||
if ((*a == '/') || (*a == '\\'))
|
||||
a++;
|
||||
|
||||
// html?
|
||||
if ((ext_chg != 0) ? (ishtml_ext(ext) == 1) : (ishtml(opt, fil) == 1)) {
|
||||
if (opt->savename_type % 100 == 5)
|
||||
strcatbuff(afs->save, "html/");
|
||||
} else {
|
||||
const char *a = hts_lastcharptr(fil);
|
||||
|
||||
while ((a > fil) && (*a != '/') && (*a != '.'))
|
||||
// passer structures
|
||||
while((a > fil) && (*a != '/') && (*a != '\\'))
|
||||
a--;
|
||||
if (*a != '.')
|
||||
strcatbuff(afs->save, "other");
|
||||
else
|
||||
strcatbuff(afs->save, a + 1);
|
||||
strcatbuff(afs->save, "/");
|
||||
}
|
||||
if ((*a == '/') || (*a == '\\'))
|
||||
a++;
|
||||
|
||||
// html?
|
||||
if ((ext_chg != 0) ? (ishtml_ext(ext) == 1) : (ishtml(opt, fil) == 1)) {
|
||||
if (opt->savename_type % 100 == 5)
|
||||
strcatbuff(afs->save, "html/");
|
||||
} else {
|
||||
const char *a = fil + strlen(fil) - 1;
|
||||
|
||||
while((a > fil) && (*a != '/') && (*a != '.'))
|
||||
a--;
|
||||
if (*a != '.')
|
||||
strcatbuff(afs->save, "other");
|
||||
else
|
||||
strcatbuff(afs->save, a + 1);
|
||||
strcatbuff(afs->save, "/");
|
||||
}
|
||||
/*strcatbuff(save,a); */
|
||||
/* add name */
|
||||
ADD_STANDARD_NAME(0);
|
||||
@@ -1258,7 +1258,7 @@ int url_savename(lien_adrfilsave *const afs,
|
||||
}
|
||||
afs->save[i + j] = '\0';
|
||||
// ajouter extension
|
||||
a = hts_lastcharptr(fil);
|
||||
a = fil + strlen(fil) - 1;
|
||||
while((a > fil) && (*a != '/') && (*a != '.'))
|
||||
a--;
|
||||
if (*a == '.') {
|
||||
@@ -1303,7 +1303,7 @@ int url_savename(lien_adrfilsave *const afs,
|
||||
// cela évite les /chez/toto et les /chez/toto/index.html incompatibles
|
||||
if (opt->savename_type != -1 &&
|
||||
opt->savename_delayed != HTS_SAVENAME_DELAYED_HARD) {
|
||||
char *a = hts_lastcharptr(afs->save);
|
||||
char *a = afs->save + strlen(afs->save) - 1;
|
||||
|
||||
while((a > afs->save) && (*a != '.') && (*a != '/'))
|
||||
a--;
|
||||
@@ -1420,10 +1420,8 @@ int url_savename(lien_adrfilsave *const afs,
|
||||
if (opt->savename_83 > 0) {
|
||||
char *a, *last;
|
||||
|
||||
for (last = hts_lastcharptr(afs->save);
|
||||
last != afs->save && *last != '/' && *last != '\\' && *last != '.';
|
||||
last--)
|
||||
;
|
||||
for(last = afs->save + strlen(afs->save) - 1;
|
||||
last != afs->save && *last != '/' && *last != '\\' && *last != '.'; last--) ;
|
||||
if (*last != '.') {
|
||||
last = NULL;
|
||||
}
|
||||
@@ -1678,8 +1676,8 @@ int url_savename(lien_adrfilsave *const afs,
|
||||
#endif
|
||||
} else { // utilisé par un AUTRE, changer de nom
|
||||
char BIGSTK tempo[HTS_URLMAXSIZE * 2];
|
||||
char *a = hts_lastcharptr(afs->save);
|
||||
size_t stem;
|
||||
char *a = afs->save + strlen(afs->save) - 1;
|
||||
char *b;
|
||||
int n = 2;
|
||||
char collisionSeparator =
|
||||
((opt->savename_83 != HTS_SAVENAME_83_ISO9660) ? '-' : '_');
|
||||
@@ -1701,16 +1699,18 @@ int url_savename(lien_adrfilsave *const afs,
|
||||
strcatbuff(tempo, afs->save);
|
||||
|
||||
// tester la présence d'un -xx (ex: index-2.html -> index-3.html)
|
||||
stem = hts_rtrimlen(tempo, "0123456789");
|
||||
if (stem != 0 && tempo[stem - 1] == collisionSeparator) {
|
||||
sscanf(tempo + stem, "%d", &n);
|
||||
tempo[stem - 1] = '\0'; // couper
|
||||
b = tempo + strlen(tempo) - 1;
|
||||
while(isdigit((unsigned char) *b))
|
||||
b--;
|
||||
if (*b == collisionSeparator) {
|
||||
sscanf(b + 1, "%d", &n);
|
||||
*b = '\0'; // couper
|
||||
n++; // plus un
|
||||
}
|
||||
// en plus il faut gérer le 8-3 .. pas facile le client
|
||||
if (opt->savename_83) {
|
||||
int max;
|
||||
char *a = hts_lastcharptr(tempo);
|
||||
char *a = tempo + strlen(tempo) - 1;
|
||||
|
||||
while((a > tempo) && (*a != '/'))
|
||||
a--;
|
||||
|
||||
@@ -2057,7 +2057,7 @@ int htsparse(htsmoduleStruct * str, htsmoduleStructExtended * stre) {
|
||||
}
|
||||
q = strchr(a, '?'); // ne pas traiter après '?'
|
||||
if (!q)
|
||||
q = hts_lastcharptr(a);
|
||||
q = a + strlen(a) - 1;
|
||||
while((p = strstr(a, "//")) && (!done)) { // remplacer // par /
|
||||
if (p > q) { // après le ? (toto.cgi?param=1//2.3)
|
||||
done = 1; // stopper
|
||||
@@ -2208,7 +2208,7 @@ int htsparse(htsmoduleStruct * str, htsmoduleStructExtended * stre) {
|
||||
// OUI!!
|
||||
#if HTS_TILDE_SLASH
|
||||
if (hts_lastchar(lien) != '/') {
|
||||
char *a = hts_lastcharptr(lien);
|
||||
char *a = lien + strlen(lien) - 1;
|
||||
|
||||
// éviter aussi index~1.html
|
||||
while(a > lien && (*a != '~') && (*a != '/')
|
||||
@@ -2254,7 +2254,7 @@ int htsparse(htsmoduleStruct * str, htsmoduleStructExtended * stre) {
|
||||
// vérifier que l'on ne doit pas ajouter de .class
|
||||
if (!error) {
|
||||
if (add_class) {
|
||||
char *a = hts_lastcharptr(lien);
|
||||
char *a = lien + strlen(lien) - 1;
|
||||
|
||||
while((a > lien) && (*a != '/') && (*a != '.'))
|
||||
a--;
|
||||
@@ -2309,7 +2309,7 @@ int htsparse(htsmoduleStruct * str, htsmoduleStructExtended * stre) {
|
||||
{
|
||||
char *a;
|
||||
|
||||
a = hts_lastcharptr(lien);
|
||||
a = lien + strlen(lien) - 1;
|
||||
while((*a) && (*a != '/') && (a > lien))
|
||||
a--;
|
||||
if (*a == '/') {
|
||||
@@ -2320,8 +2320,8 @@ int htsparse(htsmoduleStruct * str, htsmoduleStructExtended * stre) {
|
||||
}
|
||||
|
||||
if (!error) { // pas d'erreur?
|
||||
if (p_type == 2) { // code ET PAS codebase
|
||||
char *a = hts_lastcharptr(lien);
|
||||
if (p_type == 2) { // code ET PAS codebase
|
||||
char *a = lien + strlen(lien) - 1;
|
||||
char *start_of_filename = jump_identification(lien);
|
||||
|
||||
if (start_of_filename != NULL
|
||||
|
||||
@@ -569,38 +569,6 @@ static HTS_INLINE HTS_UNUSED hts_boolean hts_choplastchar(char *s) {
|
||||
return HTS_FALSE;
|
||||
}
|
||||
|
||||
/* htslib.h's is_space() and is_realspace() as hts_rtrim() sets; -#test=rtrim
|
||||
keeps them in sync. */
|
||||
#define HTS_SPACES " \"\n\r\t\f\v'"
|
||||
#define HTS_REALSPACES " \n\r\t\f\v"
|
||||
|
||||
/* Length of s once its trailing bytes from set are dropped; 0 if they all are.
|
||||
Counts down from the end, so it stops at s rather than below the buffer. */
|
||||
static HTS_INLINE HTS_UNUSED size_t hts_rtrimlen(const char *s,
|
||||
const char *set) {
|
||||
size_t len = strlen(s);
|
||||
|
||||
while (len != 0 && strchr(set, s[len - 1]) != NULL)
|
||||
len--;
|
||||
return len;
|
||||
}
|
||||
|
||||
/* Drop the trailing bytes of s that occur in set. */
|
||||
static HTS_INLINE HTS_UNUSED void hts_rtrim(char *s, const char *set) {
|
||||
s[hts_rtrimlen(s, set)] = '\0';
|
||||
}
|
||||
|
||||
/* Offset of the last character of s, or 0 when s is empty. */
|
||||
static HTS_INLINE HTS_UNUSED size_t hts_lastcharoffset(const char *s) {
|
||||
const size_t len = strlen(s);
|
||||
|
||||
return len != 0 ? len - 1 : 0;
|
||||
}
|
||||
|
||||
/* Address of the last character of S, or of its terminating NUL when S is
|
||||
empty. S is evaluated twice, so pass an lvalue. */
|
||||
#define hts_lastcharptr(S) ((S) + hts_lastcharoffset(S))
|
||||
|
||||
/* Thin aliases over the libc allocator/memcpy (historical "t" suffix); no
|
||||
added bounds checking. freet() also NULLs the freed pointer and tolerates
|
||||
NULL. memcpybuff() despite the name is a raw memcpy: the caller owns the
|
||||
|
||||
@@ -2659,8 +2659,11 @@ static int st_logcallback(httrackp *opt, int argc, char **argv) {
|
||||
strcpybuff(seen, st_log_callback_seen);
|
||||
|
||||
rewind(fp);
|
||||
line[0] = '\0';
|
||||
(void) fgets(line, (int) sizeof(line), 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;
|
||||
@@ -6137,10 +6140,9 @@ static hts_boolean ro_is(const char *path, const char *data) {
|
||||
}
|
||||
|
||||
// -#test=renameover <dir>: hts_rename_over() must replace an existing dst and
|
||||
// never delete one it did not replace (#779). Which half is live depends on
|
||||
// never lose one it did not replace (#779, #790). 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;
|
||||
@@ -6169,7 +6171,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(src, dst)) {
|
||||
if (!hts_rename_over(opt, src, dst)) {
|
||||
fprintf(stderr, "renameover: replacing an existing dst failed: %s\n",
|
||||
strerror(errno));
|
||||
err++;
|
||||
@@ -6179,7 +6181,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(src, dst)) {
|
||||
if (hts_rename_over(opt, src, dst)) {
|
||||
fprintf(stderr, "renameover: an unfixable failure reported success\n");
|
||||
err++;
|
||||
}
|
||||
@@ -6189,10 +6191,36 @@ 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(src, dst)) {
|
||||
if (hts_rename_over(opt, src, dst)) {
|
||||
fprintf(stderr, "renameover: a missing src reported success\n");
|
||||
err++;
|
||||
}
|
||||
@@ -6203,11 +6231,88 @@ 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(src, dst)) {
|
||||
if (hts_rename_over(opt, 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;
|
||||
@@ -6589,6 +6694,109 @@ 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
|
||||
|
||||
@@ -6710,10 +6918,9 @@ static int st_changes_race(httrackp *opt, int argc, char **argv) {
|
||||
return err;
|
||||
}
|
||||
|
||||
/* The x[strlen(x) - 1] class (#770) and its pointer spelling x + strlen(x) - 1
|
||||
(#781). The string starts mid-arena so the byte it must not touch is a real
|
||||
neighbour; poisoned with '#', not 0, or a stray NUL terminator would read as
|
||||
untouched. */
|
||||
/* The x[strlen(x) - 1] class (#770). The string starts mid-arena so the byte
|
||||
it must not touch is a real neighbour; poisoned with '#', not 0, or a stray
|
||||
NUL terminator would read as untouched. */
|
||||
static int st_lastchar(httrackp *opt, int argc, char **argv) {
|
||||
enum { off = 8 };
|
||||
|
||||
@@ -6780,52 +6987,11 @@ static int st_lastchar(httrackp *opt, int argc, char **argv) {
|
||||
CHECK(s[0] == '\0');
|
||||
CHECK(arena[guard] == '#');
|
||||
|
||||
/* the pointer spelling (#781): on an empty string the address must be the
|
||||
terminating NUL, never the byte before it */
|
||||
REPOISON("");
|
||||
CHECK(hts_lastcharoffset(s) == 0);
|
||||
CHECK(hts_lastcharptr(s) == s);
|
||||
CHECK(*hts_lastcharptr(s) == '\0');
|
||||
*hts_lastcharptr(s) = 'Z'; /* a write through it must stay inside s */
|
||||
CHECK(arena[guard] == '#');
|
||||
CHECK(s[0] == 'Z');
|
||||
|
||||
/* the neighbour must not be mistaken for the string's own last byte */
|
||||
REPOISON("");
|
||||
arena[guard] = '/';
|
||||
CHECK(hts_lastcharptr(s) == s);
|
||||
CHECK(*hts_lastcharptr(s) != '/');
|
||||
CHECK(arena[guard] == '/');
|
||||
|
||||
/* the walk-back loops the sites use must stop at once on an empty string */
|
||||
REPOISON("");
|
||||
{
|
||||
const char *p = hts_lastcharptr(s);
|
||||
int steps = 0;
|
||||
|
||||
while (p > s && *p != '/')
|
||||
p--, steps++;
|
||||
CHECK(steps == 0);
|
||||
CHECK(p == s);
|
||||
}
|
||||
|
||||
REPOISON("ab/");
|
||||
CHECK(hts_lastcharoffset(s) == 2);
|
||||
CHECK(hts_lastcharptr(s) == s + 2);
|
||||
CHECK(*hts_lastcharptr(s) == '/');
|
||||
REPOISON("/");
|
||||
CHECK(hts_lastcharptr(s) == s);
|
||||
CHECK(*hts_lastcharptr(s) == '/');
|
||||
CHECK(arena[guard] == '#');
|
||||
|
||||
/* control: the canary must be able to fail, or the checks above prove
|
||||
nothing. Clobber it exactly as the unguarded idiom would. */
|
||||
REPOISON("");
|
||||
s[-1] = '\0';
|
||||
CHECK(arena[guard] != '#');
|
||||
REPOISON("");
|
||||
*(s + strlen(s) - 1) = 'X';
|
||||
CHECK(arena[guard] != '#');
|
||||
|
||||
#undef REPOISON
|
||||
#undef CHECK
|
||||
@@ -6834,91 +7000,6 @@ static int st_lastchar(httrackp *opt, int argc, char **argv) {
|
||||
return err;
|
||||
}
|
||||
|
||||
/* hts_rtrim() and the sets it is called with. The string starts mid-arena, and
|
||||
the byte below it is poisoned with '#' rather than 0, or the stray NUL the
|
||||
old loop wrote there would read as untouched. */
|
||||
static int st_rtrim(httrackp *opt, int argc, char **argv) {
|
||||
enum { off = 8 };
|
||||
|
||||
char arena[24];
|
||||
char *const s = &arena[off];
|
||||
const int guard = off - 1;
|
||||
int err = 0;
|
||||
int c;
|
||||
|
||||
(void) opt;
|
||||
(void) argc;
|
||||
(void) argv;
|
||||
|
||||
#define REPOISON(str) \
|
||||
do { \
|
||||
memset(arena, '#', sizeof(arena)); \
|
||||
strlcpybuff(s, (str), sizeof(arena) - off); \
|
||||
} while (0)
|
||||
#define CHECK(cond) \
|
||||
do { \
|
||||
if (!(cond)) { \
|
||||
printf(" FAIL line %d: %s\n", __LINE__, #cond); \
|
||||
err = 1; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
/* nothing but spaces: the case that ran the old loop off the front */
|
||||
REPOISON(" ");
|
||||
hts_rtrim(s, HTS_REALSPACES);
|
||||
CHECK(s[0] == '\0');
|
||||
CHECK(arena[guard] == '#');
|
||||
|
||||
/* a space sitting below the string must not be eaten as if it were part of
|
||||
it, which is exactly what the old loop did */
|
||||
REPOISON(" ");
|
||||
arena[guard] = ' ';
|
||||
hts_rtrim(s, HTS_REALSPACES);
|
||||
CHECK(s[0] == '\0');
|
||||
CHECK(arena[guard] == ' ');
|
||||
|
||||
REPOISON("");
|
||||
hts_rtrim(s, HTS_REALSPACES);
|
||||
CHECK(s[0] == '\0');
|
||||
CHECK(arena[guard] == '#');
|
||||
|
||||
REPOISON("a b \t\r\n");
|
||||
hts_rtrim(s, HTS_REALSPACES);
|
||||
CHECK(strcmp(s, "a b") == 0);
|
||||
CHECK(arena[guard] == '#');
|
||||
|
||||
REPOISON("ab");
|
||||
hts_rtrim(s, HTS_REALSPACES);
|
||||
CHECK(strcmp(s, "ab") == 0);
|
||||
|
||||
/* quotes count as space for is_space() but not for is_realspace() */
|
||||
REPOISON("v\" ");
|
||||
hts_rtrim(s, HTS_REALSPACES);
|
||||
CHECK(strcmp(s, "v\"") == 0);
|
||||
REPOISON("v\" ");
|
||||
hts_rtrim(s, HTS_SPACES);
|
||||
CHECK(strcmp(s, "v") == 0);
|
||||
|
||||
/* the sets must stay the macros they stand for */
|
||||
for (c = 1; c < 256; c++) {
|
||||
const char b = (char) c;
|
||||
|
||||
CHECK((strchr(HTS_SPACES, b) != NULL) == (is_space(b) != 0));
|
||||
CHECK((strchr(HTS_REALSPACES, b) != NULL) == (is_realspace(b) != 0));
|
||||
}
|
||||
|
||||
/* control: the canary must be able to fail */
|
||||
REPOISON(" ");
|
||||
s[-1] = '\0';
|
||||
CHECK(arena[guard] != '#');
|
||||
|
||||
#undef REPOISON
|
||||
#undef CHECK
|
||||
|
||||
printf("rtrim self-test: %s\n", err ? "FAIL" : "OK");
|
||||
return err;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/* Registry: name -> handler, with a usage hint and a one-line description. */
|
||||
/* ------------------------------------------------------------ */
|
||||
@@ -6974,16 +7055,16 @@ static const struct selftest_entry {
|
||||
{"strsafe", "[overflow|overflow-buff|overflow-src [str]]",
|
||||
"bounded string-op self-test", st_strsafe},
|
||||
{"copyopt", "", "copy_htsopt option-copy self-test", st_copyopt},
|
||||
{"lastchar", "",
|
||||
"last-char helpers never index before the buffer (#770, #781)",
|
||||
{"lastchar", "", "last-char helpers never index before the buffer (#770)",
|
||||
st_lastchar},
|
||||
{"rtrim", "", "hts_rtrim never walks below the buffer", st_rtrim},
|
||||
{"changes", "", "--changes bucket accounting and JSON escaping (#714)",
|
||||
st_changes},
|
||||
{"changes-race", "<dir>", "--changes under a late transfer thread (#714)",
|
||||
st_changes_race},
|
||||
{"threadwait", "", "htsthread_wait() joins threads spawned just before it",
|
||||
st_threadwait},
|
||||
{"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},
|
||||
|
||||
@@ -1109,7 +1109,7 @@ hts_boolean singlefile_rewrite_file(httrackp *opt, const char *root,
|
||||
HTS_ACCESS_FILE);
|
||||
#endif
|
||||
if (ok)
|
||||
ok = hts_rename_over(StringBuff(tmp), page_path);
|
||||
ok = hts_rename_over(opt, StringBuff(tmp), page_path);
|
||||
if (!ok) {
|
||||
hts_log_print(opt, LOG_ERROR, "single-file: could not rewrite %s",
|
||||
page_path);
|
||||
|
||||
@@ -324,7 +324,8 @@ int lienrelatif(char *s, size_t ssize, const char *link, const char *curr_fil) {
|
||||
curr = _curr;
|
||||
strlcpybuff(curr, curr_fil, sizeof(_curr));
|
||||
if ((a = strchr(curr, '?')) == NULL) { // cut at the ? (query parameters)
|
||||
a = hts_lastcharptr(curr);
|
||||
// an empty path has no last character: curr-1 would read before the buffer
|
||||
a = curr[0] != '\0' ? curr + strlen(curr) - 1 : curr;
|
||||
}
|
||||
while((*a != '/') && (a > curr))
|
||||
a--; // chercher dernier / du chemin courant
|
||||
@@ -1437,21 +1438,75 @@ HTSEXT_API hts_boolean hts_findissystem(find_handle find) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
hts_boolean hts_rename_over(const char *src, const char *dst) {
|
||||
/* 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) {
|
||||
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 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. */
|
||||
/* 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. */
|
||||
const int err = errno;
|
||||
|
||||
if (err != EEXIST || !fexist_utf8(src))
|
||||
return HTS_FALSE;
|
||||
(void) UNLINK(cdst);
|
||||
return RENAME(csrc, cdst) == 0 ? HTS_TRUE : HTS_FALSE;
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -137,11 +137,17 @@ 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. 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);
|
||||
/* 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);
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
@@ -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(tmppath, waczpath)) {
|
||||
} else if (!hts_rename_over(w->opt, 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,16 +1392,8 @@ warc_writer *warc_open(httrackp *opt, const char *path) {
|
||||
char ts[32];
|
||||
time_t t = time(NULL);
|
||||
struct tm tmv;
|
||||
#if defined(_WIN32)
|
||||
struct tm *g = gmtime(&t);
|
||||
if (g != NULL)
|
||||
tmv = *g;
|
||||
else
|
||||
if (!hts_gmtime(t, &tmv))
|
||||
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 =
|
||||
@@ -1589,7 +1581,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(tmpbuf, final)) {
|
||||
if (!hts_rename_over(w->opt, tmpbuf, final)) {
|
||||
hts_log_print(w->opt, LOG_ERROR | LOG_ERRNO,
|
||||
"WARC: could not replace %s", final);
|
||||
return HTS_FALSE;
|
||||
|
||||
@@ -520,12 +520,13 @@ 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 *timetm;
|
||||
struct tm timetmbuf;
|
||||
struct tm *timetm = &timetmbuf;
|
||||
|
||||
if (timestamp == (time_t) 0 || timestamp == (time_t) - 1) {
|
||||
timestamp = time(NULL);
|
||||
}
|
||||
if ((timetm = gmtime(×tamp)) != NULL) {
|
||||
if (hts_gmtime(timestamp, timetm)) {
|
||||
char tms[256 + 1];
|
||||
const char *name;
|
||||
|
||||
@@ -747,7 +748,8 @@ static PT_Element proxytrack_process_DAV_Request(PT_Indexes indexes,
|
||||
const char *thisUrl = list[i];
|
||||
const char *mimeType = "application/octet-stream";
|
||||
unsigned int thisUrlLen = (unsigned int) strlen(thisUrl);
|
||||
int thisIsDir = (thisUrl[thisUrlLen - 1] == '/') ? 1 : 0;
|
||||
/* the folder's default document is enumerated as an empty name */
|
||||
int thisIsDir = (hts_lastchar(thisUrl) == '/') ? 1 : 0;
|
||||
|
||||
/* Item URL */
|
||||
StringRoom(itemUrl,
|
||||
@@ -852,8 +854,7 @@ static PT_Element proxytrack_process_HTTP_List(PT_Indexes indexes,
|
||||
for(isDir = 1; isDir >= 0; isDir--) {
|
||||
for(i = 0; list[i] != NULL; i++) {
|
||||
char *thisUrl = list[i];
|
||||
unsigned int thisUrlLen = (unsigned int) strlen(thisUrl);
|
||||
int thisIsDir = (thisUrl[thisUrlLen - 1] == '/') ? 1 : 0;
|
||||
int thisIsDir = (hts_lastchar(thisUrl) == '/') ? 1 : 0;
|
||||
|
||||
if (thisIsDir == isDir) {
|
||||
if (isDir)
|
||||
|
||||
@@ -360,21 +360,14 @@ 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;
|
||||
|
||||
#ifdef _WIN32
|
||||
struct tm *tm = gmtime(&t);
|
||||
#else
|
||||
struct tm *tm = gmtime_r(&t, &tmbuf);
|
||||
#endif
|
||||
if (tm != NULL)
|
||||
return *tm;
|
||||
else {
|
||||
if (!hts_gmtime(t, &tmbuf)) {
|
||||
/* 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;
|
||||
|
||||
@@ -903,11 +903,10 @@ int PT_LoadCache__New(PT_Index index_, const char *filename) {
|
||||
coucal hashtable = index->hash;
|
||||
|
||||
/* Compute base path for this index - the filename MUST be absolute! */
|
||||
for (slashes = 2, abpath = hts_lastcharptr(filename);
|
||||
abpath > filename &&
|
||||
((*abpath != '/' && *abpath != '\\') || --slashes > 0);
|
||||
abpath--)
|
||||
;
|
||||
for(slashes = 2, abpath = filename + (int) strlen(filename) - 1;
|
||||
abpath > filename && ((*abpath != '/' && *abpath != '\\')
|
||||
|| --slashes > 0);
|
||||
abpath--) ;
|
||||
index->path[0] = '\0';
|
||||
if (slashes == 0 && *abpath != 0) {
|
||||
int i;
|
||||
@@ -1500,11 +1499,10 @@ static int PT_LoadCache__Old(PT_Index index_, const char *filename) {
|
||||
|
||||
/* -------------------- COPY OF THE __New() CODE -------------------- */
|
||||
/* Compute base path for this index - the filename MUST be absolute! */
|
||||
for (slashes = 2, abpath = hts_lastcharptr(filename);
|
||||
abpath > filename &&
|
||||
((*abpath != '/' && *abpath != '\\') || --slashes > 0);
|
||||
abpath--)
|
||||
;
|
||||
for(slashes = 2, abpath = filename + (int) strlen(filename) - 1;
|
||||
abpath > filename && ((*abpath != '/' && *abpath != '\\')
|
||||
|| --slashes > 0);
|
||||
abpath--) ;
|
||||
index->path[0] = '\0';
|
||||
if (slashes == 0 && *abpath != 0) {
|
||||
int i;
|
||||
|
||||
@@ -4,4 +4,5 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
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" <<<"$out"
|
||||
|
||||
@@ -5,4 +5,5 @@ set -euo pipefail
|
||||
|
||||
# A ready slot still owning a temporary must stay in memory: swapping it out
|
||||
# clears the entry, which unlinks the re-fetch backup (#771).
|
||||
httrack -O /dev/null -#test=backswap | grep -q "backswap self-test: OK"
|
||||
out=$(httrack -O /dev/null -#test=backswap)
|
||||
grep -q "backswap self-test: OK" <<<"$out"
|
||||
|
||||
@@ -3,27 +3,14 @@
|
||||
|
||||
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.
|
||||
|
||||
@@ -5,4 +5,5 @@ 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.
|
||||
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" <<<"$out"
|
||||
|
||||
@@ -9,4 +9,5 @@ set -euo pipefail
|
||||
dir=$(mktemp -d)
|
||||
trap 'set +e; rm -rf "$dir"' EXIT
|
||||
|
||||
httrack -O /dev/null -#test=cookieimport "$dir" | grep -q "cookieimport:.*OK"
|
||||
out=$(httrack -O /dev/null -#test=cookieimport "$dir")
|
||||
grep -q "cookieimport:.*OK" <<<"$out"
|
||||
|
||||
@@ -8,4 +8,5 @@ set -euo pipefail
|
||||
dir=$(mktemp -d)
|
||||
trap 'set +e; rm -rf "$dir"' EXIT
|
||||
|
||||
httrack -O /dev/null -#test=direnum "$dir" | grep -q "direnum:.*OK"
|
||||
out=$(httrack -O /dev/null -#test=direnum "$dir")
|
||||
grep -q "direnum:.*OK" <<<"$out"
|
||||
|
||||
@@ -4,4 +4,5 @@
|
||||
set -euo pipefail
|
||||
|
||||
# HT_ADD_HTMLESCAPED* must reserve the escaper's worst case (6 for _full).
|
||||
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" <<<"$out"
|
||||
|
||||
@@ -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.
|
||||
find "$mir/file" -name index.html | grep -q . || {
|
||||
test -n "$(find "$mir/file" -name index.html)" || {
|
||||
echo "page not mirrored; the oversized-footer path was not exercised" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -4,4 +4,5 @@
|
||||
set -euo pipefail
|
||||
|
||||
# get_ftp_line bounds a hostile CRLF-less FTP reply into its 1024-byte buffer.
|
||||
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" <<<"$out"
|
||||
|
||||
@@ -4,4 +4,5 @@
|
||||
set -euo pipefail
|
||||
|
||||
# ftp_split_userpass bounds an over-long user:pass@ from a hostile ftp:// URL.
|
||||
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" <<<"$out"
|
||||
|
||||
21
tests/01_engine-gmtime.test
Normal file
21
tests/01_engine-gmtime.test
Normal file
@@ -0,0 +1,21 @@
|
||||
#!/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
|
||||
@@ -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
|
||||
printf '%s\n' "$run" | grep -q 'zzmarker' || {
|
||||
grep -q 'zzmarker' <<<"$run" || {
|
||||
echo "FAIL: -%S rules file was not loaded"
|
||||
printf '%s\n' "$run"
|
||||
exit 1
|
||||
|
||||
@@ -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)
|
||||
printf '%s\n' "$out" | grep -q "all hashtable tests were successful!" || exit 1
|
||||
grep -q "all hashtable tests were successful!" <<<"$out" || exit 1
|
||||
|
||||
@@ -4,4 +4,5 @@
|
||||
set -euo pipefail
|
||||
|
||||
# inplace_escape_*() must match escape_*() on a copy: guards the shared helper.
|
||||
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" <<<"$out"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Runs the lastchar self-test (#770, #781), then keeps the idiom it replaced
|
||||
# from coming back: x[strlen(x) - 1] and x + strlen(x) - 1 both land one byte
|
||||
# before the buffer when x is empty, and the guard is never where they are.
|
||||
# Runs the lastchar self-test (#770), then keeps the idiom it replaced from
|
||||
# coming back: x[strlen(x) - 1] indexes one byte before the buffer when x is
|
||||
# empty, and the guard is never where the index is.
|
||||
|
||||
set -eu
|
||||
|
||||
@@ -33,36 +33,3 @@ test -z "$hits" || {
|
||||
echo "$hits" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Same for the pointer spelling. The three survivors sit in commented-out
|
||||
# blocks in htsname.c; the rest are this class being named, plus the self-test's
|
||||
# own deliberate underflow.
|
||||
hits=$(command grep -rnE '\+ *\(?(int|size_t)?\)? *strlen *\([^)]*\) *- *1' \
|
||||
"$top_srcdir/src" --include='*.c' --include='*.h' --exclude-dir=coucal |
|
||||
grep -v 'htsname\.c:.*char\* a=\(fil+strlen(fil)\|save+strlen(save)\)-1;' |
|
||||
grep -v 'htsselftest\.c:.*pointer spelling x + strlen(x) - 1' |
|
||||
grep -v 'htsselftest\.c:.*\*(s + strlen(s) - 1) =' || true)
|
||||
|
||||
test -z "$hits" || {
|
||||
echo "x + strlen(x) - 1 reintroduced; use hts_lastcharptr:" >&2
|
||||
echo "$hits" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# url_savename() has nine of these sites and reaches all of them with an empty
|
||||
# "fil", which "?x=1" produces. Names are asserted loosely, since the point is
|
||||
# the run: on unfixed code the sanitized CI leg aborts inside htsname.c.
|
||||
for args in "?x=1 text/plain" "?x=1 text/html" "?x=1 text/html type=4"; do
|
||||
# shellcheck disable=SC2086
|
||||
name=$(httrack -#test=savename $args) || {
|
||||
echo "httrack -#test=savename $args exited non-zero: $name" >&2
|
||||
exit 1
|
||||
}
|
||||
case "$name" in
|
||||
"savename: "?*) ;;
|
||||
*)
|
||||
echo "-#test=savename $args produced no name: $name" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
@@ -4,4 +4,5 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
httrack -O /dev/null -#test=logcallback | grep -q "logcallback self-test OK"
|
||||
out=$(httrack -O /dev/null -#test=logcallback)
|
||||
grep -q "logcallback self-test OK" <<<"$out"
|
||||
|
||||
@@ -8,4 +8,5 @@ set -euo pipefail
|
||||
dir=$(mktemp -d)
|
||||
trap 'set +e; rm -rf "$dir"' EXIT
|
||||
|
||||
httrack -O /dev/null -#test=longpath "$dir" | grep -q "longpath:.*OK"
|
||||
out=$(httrack -O /dev/null -#test=longpath "$dir")
|
||||
grep -q "longpath:.*OK" <<<"$out"
|
||||
|
||||
@@ -9,4 +9,5 @@ set -euo pipefail
|
||||
dir=$(mktemp -d)
|
||||
trap 'set +e; rm -rf "$dir"' EXIT
|
||||
|
||||
httrack -O /dev/null -#test=mirrorio "$dir" | grep -q "mirrorio:.*OK"
|
||||
out=$(httrack -O /dev/null -#test=mirrorio "$dir")
|
||||
grep -q "mirrorio:.*OK" <<<"$out"
|
||||
|
||||
@@ -6,4 +6,5 @@ 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.
|
||||
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" <<<"$out"
|
||||
|
||||
@@ -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 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.
|
||||
# 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.
|
||||
dir=$(mktemp -d)
|
||||
trap 'set +e; rm -rf "$dir"' EXIT
|
||||
|
||||
@@ -16,8 +16,9 @@ MINGW* | MSYS_NT*) want=fallback ;; # native rename() refuses an existing target
|
||||
esac
|
||||
|
||||
out=$(httrack -O /dev/null -#test=renameover "$dir")
|
||||
echo "$out" | grep -q "renameover: OK"
|
||||
echo "$out" | grep -q "renameover: regime $want"
|
||||
grep -q "renameover: OK" <<<"$out"
|
||||
grep -q "renameover: regime $want" <<<"$out"
|
||||
grep -q "renameover: restore back" <<<"$out"
|
||||
|
||||
if [ "$(uname -s)" != "Linux" ]; then
|
||||
echo "renameover: LD_PRELOAD interposition is Linux-only here, skipping"
|
||||
@@ -42,14 +43,29 @@ 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 unlink fallback is dead code on POSIX, so borrow Windows' rename().
|
||||
# The aside fallback is dead code on POSIX, so borrow Windows' rename().
|
||||
out=$(LD_PRELOAD="$RENAMEFAIL_LIB" httrack -O /dev/null \
|
||||
-#test=renameover "$dir")
|
||||
echo "$out" | grep -q "renameover: OK"
|
||||
echo "$out" | grep -q "renameover: regime fallback"
|
||||
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"
|
||||
|
||||
# A source another process holds fails with EACCES, which dst had no part in.
|
||||
out=$(RENAMEFAIL_MODE=locked LD_PRELOAD="$RENAMEFAIL_LIB" httrack -O /dev/null \
|
||||
-#test=renameover "$dir")
|
||||
echo "$out" | grep -q "renameover: OK"
|
||||
echo "$out" | grep -q "renameover: regime refused"
|
||||
grep -q "renameover: OK" <<<"$out"
|
||||
grep -q "renameover: regime refused" <<<"$out"
|
||||
|
||||
@@ -4,4 +4,5 @@
|
||||
set -euo pipefail
|
||||
|
||||
# robots.txt RFC 9309 Allow/Disallow precedence (#452): longest match wins.
|
||||
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" <<<"$out"
|
||||
|
||||
@@ -7,11 +7,11 @@ set -eu
|
||||
|
||||
# Bare -#test lists known tests (printed to stderr).
|
||||
list=$(httrack -#test 2>&1)
|
||||
printf '%s\n' "$list" | grep -q "filter" || exit 1
|
||||
printf '%s\n' "$list" | grep -q "cache-writefail" || exit 1
|
||||
grep -q "filter" <<<"$list" || exit 1
|
||||
grep -q "cache-writefail" <<<"$list" || exit 1
|
||||
|
||||
# Unknown name: non-zero exit + diagnostic, and no test result line.
|
||||
rc=0
|
||||
err=$(httrack -#test=bogus 2>&1) || rc=$?
|
||||
test "$rc" -ne 0 || exit 1
|
||||
printf '%s\n' "$err" | grep -q "Unknown self-test" || exit 1
|
||||
grep -q "Unknown self-test" <<<"$err" || exit 1
|
||||
|
||||
@@ -6,4 +6,5 @@ set -euo pipefail
|
||||
# The SOCKS5 handshake framing and credential split, driven against scripted
|
||||
# server replies (frame draining, oversize rejects, RFC 1929 fields).
|
||||
|
||||
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" <<<"$out"
|
||||
|
||||
@@ -4,4 +4,5 @@
|
||||
set -euo pipefail
|
||||
|
||||
# HTTP status -> reason phrase, including the modern 429/451 (#453).
|
||||
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" <<<"$out"
|
||||
|
||||
@@ -5,4 +5,5 @@ 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).
|
||||
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" <<<"$out"
|
||||
|
||||
@@ -3,27 +3,14 @@
|
||||
|
||||
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
|
||||
|
||||
@@ -4,4 +4,5 @@
|
||||
set -euo pipefail
|
||||
|
||||
# Entity/URL unescapers reserve one byte for the trailing NUL (no 1-byte OOB).
|
||||
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" <<<"$out"
|
||||
|
||||
@@ -5,4 +5,5 @@ 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).
|
||||
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" <<<"$out"
|
||||
|
||||
@@ -4,4 +4,5 @@
|
||||
set -euo pipefail
|
||||
|
||||
# Default User-Agent (#449): honest HTTrack token, no Windows 98 relic.
|
||||
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" <<<"$out"
|
||||
|
||||
@@ -5,4 +5,5 @@ 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.
|
||||
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" <<<"$out"
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Keep this POSIX-portable: the harness runs it via $(BASH), which is a plain
|
||||
# POSIX /bin/sh on some platforms (e.g. macOS), so avoid bashisms and GNU-only
|
||||
# tool flags despite the #!/bin/bash above.
|
||||
# Stick to POSIX tool flags: macOS ships BSD grep/sed, not the GNU ones.
|
||||
|
||||
# Cache write-failure policy (-#test=cache-writefail <dir>). #174/#219: disk
|
||||
# full or a failure streak aborts cleanly; an isolated failure or an oversized
|
||||
@@ -17,13 +15,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.
|
||||
printf '%s\n' "$out" | grep -qx "cache-writefail: OK" || {
|
||||
grep -qx "cache-writefail: OK" <<<"$out" || {
|
||||
echo "expected 'cache-writefail: OK', got: $out" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# A skipped entry must be warned about with its URL.
|
||||
printf '%s\n' "$out" | grep -q "entry not cached: example.com/" || {
|
||||
grep -q "entry not cached: example.com/" <<<"$out" || {
|
||||
echo "expected a URL-bearing skip warning" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Keep this POSIX-portable: the harness runs it via $(BASH), which is a plain
|
||||
# POSIX /bin/sh on some platforms (e.g. macOS), so avoid bashisms and GNU-only
|
||||
# tool flags despite the #!/bin/bash above.
|
||||
# Stick to POSIX tool flags: macOS ships BSD grep/sed, not the GNU ones.
|
||||
|
||||
# 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
|
||||
@@ -15,7 +13,7 @@ trap 'set +e; rm -rf "$dir"' EXIT
|
||||
|
||||
out=$(httrack -#test=zip-repair-shift "$dir")
|
||||
|
||||
printf '%s\n' "$out" | grep -qx "zip-repair-shift: OK (recovered 1 entry)" || {
|
||||
grep -qx "zip-repair-shift: OK (recovered 1 entry)" <<<"$out" || {
|
||||
echo "expected 'zip-repair-shift: OK (recovered 1 entry)', got: $out" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ set -euo pipefail
|
||||
|
||||
httrack_bin=$(cd "$(dirname "$(command -v httrack)")" && pwd)/httrack
|
||||
|
||||
if ! "$httrack_bin" -#test 2>&1 | grep -q '^ warc-wacz'; then
|
||||
registry=$("$httrack_bin" -#test 2>&1 || true)
|
||||
if ! grep -q '^ warc-wacz' <<<"$registry"; then
|
||||
echo "warc-wacz self-test unavailable (build without OpenSSL); skipping"
|
||||
exit 77
|
||||
fi
|
||||
|
||||
@@ -152,13 +152,15 @@ 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
|
||||
listed unchanged | grep -qx "${host}/${name}.bin:$(size_of "${tmpdir}/snap/${name}.bin")" ||
|
||||
grep -qx "${host}/${name}.bin:$(size_of "${tmpdir}/snap/${name}.bin")" <<<"$unchanged" ||
|
||||
fail "${name}.bin is not reported unchanged at its previous size"
|
||||
if listed changed | grep -q "^${host}/${name}.bin:"; then
|
||||
if grep -q "^${host}/${name}.bin:" <<<"$changed"; then
|
||||
fail "${name}.bin is reported changed as well"
|
||||
fi
|
||||
done
|
||||
listed changed | grep -qx "${host}/stay.bin:$(size_of "${out}/${host}/stay.bin")" ||
|
||||
grep -qx "${host}/stay.bin:$(size_of "${out}/${host}/stay.bin")" <<<"$changed" ||
|
||||
fail "stay.bin is not reported changed"
|
||||
ok "the change report calls the kept files unchanged and the refreshed one changed"
|
||||
|
||||
98
tests/110_local-ftp-parallel.test
Normal file
98
tests/110_local-ftp-parallel.test
Normal file
@@ -0,0 +1,98 @@
|
||||
#!/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"
|
||||
125
tests/111_local-ftp-update-rest.test
Normal file
125
tests/111_local-ftp-update-rest.test
Normal file
@@ -0,0 +1,125 @@
|
||||
#!/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"
|
||||
108
tests/114_local-update-304-leak.test
Normal file
108
tests/114_local-update-304-leak.test
Normal file
@@ -0,0 +1,108 @@
|
||||
#!/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
|
||||
@@ -1,107 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Backward scans that start at s + strlen(s) - 1 and decrement with no lower
|
||||
# bound: the config-file right trim and the collision-rename digit scan both
|
||||
# walked off the front of a stack buffer. Neither ASan nor _FORTIFY_SOURCE sees
|
||||
# the self-test's overrun (it lands in the same frame), hence the poisoned byte.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
: "${top_srcdir:=..}"
|
||||
|
||||
# Resolve httrack before any cd: PATH may carry a build-relative entry.
|
||||
bin=$(command -v httrack) || {
|
||||
echo "FAIL: httrack not found on PATH" >&2
|
||||
exit 1
|
||||
}
|
||||
case "$bin" in
|
||||
/*) ;;
|
||||
*) bin="$(cd "$(dirname "$bin")" && pwd)/$(basename "$bin")" ;;
|
||||
esac
|
||||
|
||||
tmp=$(mktemp -d "${TMPDIR:-/tmp}/httrack_rtrim.XXXXXX") || exit 1
|
||||
trap 'set +e; rm -rf "$tmp"' EXIT
|
||||
trap 'rm -rf "$tmp"' HUP INT QUIT PIPE TERM
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- 1. the helper itself ---------------------------------------------------
|
||||
out=$("$bin" -#test=rtrim) || fail "httrack -#test=rtrim exited non-zero: $out"
|
||||
test "$out" = "rtrim self-test: OK" || fail "expected 'rtrim self-test: OK', got: $out"
|
||||
|
||||
# --- 2. collision rename over an all-digit name -----------------------------
|
||||
# Two pages with the same all-digit name save to 2024.html, so the second goes
|
||||
# through the -<n> rename and the digit scan has nothing but digits to walk
|
||||
# over. Driven twice, because the two routes differ in who picks the URLs.
|
||||
site="$tmp/site"
|
||||
mkdir -p "$site/a" "$site/b"
|
||||
printf '%s\n' '<html><body><a href="a/2024.html">a</a> <a href="b/2024.html">b</a></body></html>' \
|
||||
>"$site/index.html"
|
||||
echo '<html><body>A</body></html>' >"$site/a/2024.html"
|
||||
echo '<html><body>B</body></html>' >"$site/b/2024.html"
|
||||
|
||||
# -g flattens the tree, and pins depth to 0: the operator supplies both URLs.
|
||||
out1="$tmp/out1"
|
||||
mkdir -p "$out1"
|
||||
(cd "$out1" && "$bin" -g "file://$site/a/2024.html" "file://$site/b/2024.html" \
|
||||
--quiet -n >.log 2>&1) || fail "collision crawl exited non-zero: $(tail -3 "$out1/.log")"
|
||||
|
||||
test -f "$out1/2024.html" || fail "2024.html not mirrored"
|
||||
test -f "$out1/2024-2.html" || fail "colliding name was not renamed to 2024-2.html"
|
||||
|
||||
# -N "%n.%t" collides the same way with depth unrestricted, so one starting URL
|
||||
# is enough and the crawled page picks both colliding names itself.
|
||||
out3="$tmp/out3"
|
||||
mkdir -p "$out3"
|
||||
(cd "$out3" && "$bin" "file://$site/index.html" -N "%n.%t" --quiet -n >.log 2>&1) ||
|
||||
fail "userdef-savename crawl exited non-zero: $(tail -3 "$out3/.log")"
|
||||
test -f "$out3/2024.html" || fail "2024.html not mirrored under -N '%n.%t'"
|
||||
test -f "$out3/2024-2.html" ||
|
||||
fail "colliding name was not renamed under -N '%n.%t' (links followed, not argv)"
|
||||
|
||||
# The rename must keep counting, not restart: a third collision is -3.
|
||||
mkdir -p "$site/c"
|
||||
echo '<html><body>C</body></html>' >"$site/c/2024.html"
|
||||
out2="$tmp/out2"
|
||||
mkdir -p "$out2"
|
||||
(cd "$out2" && "$bin" -g "file://$site/a/2024.html" "file://$site/b/2024.html" \
|
||||
"file://$site/c/2024.html" --quiet -n >.log 2>&1) ||
|
||||
fail "three-way collision crawl exited non-zero: $(tail -3 "$out2/.log")"
|
||||
test -f "$out2/2024-3.html" || fail "third colliding name was not renamed to 2024-3.html"
|
||||
|
||||
# --- 3. a blank config line is skipped, not parsed ---------------------------
|
||||
# HTS_HTTRACKRC is ".httrackrc" on POSIX and "httrackrc" on Windows.
|
||||
rcdir="$tmp/rc"
|
||||
mkdir -p "$rcdir"
|
||||
echo '<html><body>hi</body></html>' >"$rcdir/index.html"
|
||||
printf ' \nzzz-not-an-option\n' >"$rcdir/.httrackrc"
|
||||
cp "$rcdir/.httrackrc" "$rcdir/httrackrc"
|
||||
|
||||
rc=0
|
||||
log=$( (cd "$rcdir" && "$bin" "file://$rcdir/index.html" --quiet -n 2>&1)) || rc=$?
|
||||
test "$rc" -eq 0 || fail "rc-file crawl exited $rc: $log"
|
||||
log=${log//$'\r'/} # the empty-option check below must not miss a CRLF line
|
||||
|
||||
# The line after the blank one must still be read, or this proves nothing.
|
||||
case "$log" in
|
||||
*"Unknown option: zzz-not-an-option"*) ;;
|
||||
*) fail "config file was not parsed past the blank line: $log" ;;
|
||||
esac
|
||||
# The blank line itself must not become an empty option.
|
||||
case "$log" in
|
||||
*"Unknown option: "$'\n'*) fail "blank config line was parsed as an empty option: $log" ;;
|
||||
esac
|
||||
|
||||
# --- 4. the call sites stay bounded -----------------------------------------
|
||||
# Sections 2 and 3 only go red under a sanitizer: on a plain build the byte
|
||||
# below the buffer is usually not a digit, so a reverted scan stops anyway and
|
||||
# produces the same names. Pin the two sites at the source instead.
|
||||
command grep -q 'hts_rtrim(line, HTS_REALSPACES)' "$top_srcdir/src/htsalias.c" ||
|
||||
fail "htsalias.c right trim no longer goes through hts_rtrim"
|
||||
command grep -q 'hts_rtrimlen(tempo, "0123456789")' "$top_srcdir/src/htsname.c" ||
|
||||
fail "htsname.c collision-suffix scan no longer goes through hts_rtrimlen"
|
||||
|
||||
exit 0
|
||||
121
tests/120_local-proxytrack-webdav-default.test
Executable file
121
tests/120_local-proxytrack-webdav-default.test
Executable file
@@ -0,0 +1,121 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# A PROPFIND whose path is an exact cache entry enumerates that entry's default
|
||||
# document under an empty name; proxytrack used to index one byte before it and
|
||||
# die on the whole listener.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
: "${top_srcdir:=..}"
|
||||
# shellcheck source=tests/testlib.sh
|
||||
. "$top_srcdir/tests/testlib.sh"
|
||||
|
||||
python=$(find_python) || {
|
||||
echo "python3 missing, skipping"
|
||||
exit 77
|
||||
}
|
||||
command -v curl >/dev/null 2>&1 || {
|
||||
echo "curl missing, skipping"
|
||||
exit 77
|
||||
}
|
||||
# MSYS cannot reap a native listener, and the orphan wedges the suite (#595).
|
||||
if is_windows; then
|
||||
echo "windows: cannot reap a backgrounded proxytrack, skipping"
|
||||
exit 77
|
||||
fi
|
||||
|
||||
dir=$(mktemp -d)
|
||||
ptpid=
|
||||
cleanup() {
|
||||
stop_server "$ptpid"
|
||||
rm -rf "$dir"
|
||||
}
|
||||
trap 'set +e; cleanup' EXIT
|
||||
trap 'set +e; cleanup; exit 1' INT TERM HUP
|
||||
|
||||
printf 'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 5\r\n\r\n' >"$dir/hdr"
|
||||
printf 'hello' >"$dir/body"
|
||||
alen=$(($(wc -c <"$dir/hdr") + $(wc -c <"$dir/body")))
|
||||
{
|
||||
printf 'filedesc://t.arc 0.0.0.0 20250101000000 text/plain 200 - - 0 t.arc 9\n'
|
||||
printf '2 0 test\n'
|
||||
printf '\n\n'
|
||||
printf 'http://example.com/page.html 0.0.0.0 20250101000000 text/html 200 - - 0 t.arc %d\n' "$alen"
|
||||
cat "$dir/hdr" "$dir/body"
|
||||
} >"$dir/in.arc"
|
||||
|
||||
freeport() {
|
||||
"$python" -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close()'
|
||||
}
|
||||
proxyport=$(freeport)
|
||||
icpport=$(freeport)
|
||||
|
||||
proxytrack "127.0.0.1:$proxyport" "127.0.0.1:$icpport" "$dir/in.arc" >"$dir/pt.log" 2>&1 &
|
||||
ptpid=$!
|
||||
waited=0
|
||||
until grep -qE "HTTP Proxy installed on|Unable to (initialize a temporary server|create the server)" "$dir/pt.log"; do
|
||||
kill -0 "$ptpid" 2>/dev/null || {
|
||||
echo "FAIL: proxytrack exited before listening"
|
||||
cat "$dir/pt.log"
|
||||
exit 1
|
||||
}
|
||||
test "$waited" -lt 50 || {
|
||||
echo "FAIL: proxytrack never announced its listen port"
|
||||
exit 1
|
||||
}
|
||||
sleep 0.1
|
||||
waited=$((waited + 1))
|
||||
done
|
||||
grep -q "HTTP Proxy installed on" "$dir/pt.log" || {
|
||||
echo "FAIL: proxytrack failed to bind"
|
||||
cat "$dir/pt.log"
|
||||
exit 1
|
||||
}
|
||||
|
||||
propfind() {
|
||||
curl -s -i --max-time 30 -X PROPFIND -H "Depth: 1" \
|
||||
"http://127.0.0.1:$proxyport/webdav/example.com/$1"
|
||||
}
|
||||
|
||||
resp=$(propfind page.html) || {
|
||||
echo "FAIL: PROPFIND on an exact cache entry did not answer"
|
||||
cat "$dir/pt.log"
|
||||
exit 1
|
||||
}
|
||||
grep -q '^HTTP/1\.[01] 207 ' <<<"$resp" || {
|
||||
echo "FAIL: expected 207 Multi-Status"
|
||||
printf '%s\n' "$resp"
|
||||
exit 1
|
||||
}
|
||||
grep -q '<displayname>Default Document for the Folder</displayname>' <<<"$resp" || {
|
||||
echo "FAIL: the default document of the entry was not listed"
|
||||
printf '%s\n' "$resp"
|
||||
exit 1
|
||||
}
|
||||
# The name alone would still pass on a truncated or garbage URL, so pin the
|
||||
# href: the default document is the collection path plus an empty name.
|
||||
grep -q '<href>/webdav/example\.com/page\.html/</href>' <<<"$resp" || {
|
||||
echo "FAIL: the default document was listed under the wrong href"
|
||||
printf '%s\n' "$resp"
|
||||
exit 1
|
||||
}
|
||||
responses=$(grep -c '<response ' <<<"$resp" || true)
|
||||
test "$responses" -eq 2 || {
|
||||
echo "FAIL: expected the root and its default document, got $responses responses"
|
||||
printf '%s\n' "$resp"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# The crash killed the process, so a second request is the liveness proof.
|
||||
resp=$(propfind "") || {
|
||||
echo "FAIL: proxytrack died serving the previous PROPFIND"
|
||||
cat "$dir/pt.log"
|
||||
exit 1
|
||||
}
|
||||
grep -q '^HTTP/1\.[01] 207 ' <<<"$resp" || {
|
||||
echo "FAIL: expected 207 Multi-Status on the follow-up listing"
|
||||
printf '%s\n' "$resp"
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "OK: PROPFIND on an exact cache entry lists its default document and survives"
|
||||
@@ -13,7 +13,10 @@ bash "$top_srcdir/tests/local-crawl.sh" --errors 1 \
|
||||
--not-found 'errpage/missing.html' \
|
||||
httrack 'BASEURL/errpage/index.html' '-o0'
|
||||
|
||||
# Control -o1 (default): the 404 error page is written.
|
||||
# Control -o1 (default): the 404 body is the server's verbatim, no HTTrack marker (#787); good.html proves the marker check can fire.
|
||||
bash "$top_srcdir/tests/local-crawl.sh" --errors 1 \
|
||||
--found 'errpage/missing.html' \
|
||||
--file-matches 'errpage/missing.html' '404 error body' \
|
||||
--file-not-matches 'errpage/missing.html' 'HTTrack' \
|
||||
--file-matches 'errpage/good.html' 'HTTrack' \
|
||||
httrack 'BASEURL/errpage/index.html' '-o1'
|
||||
|
||||
@@ -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')
|
||||
echo "${block}" | grep -q 'LANGN=.*lang_index' || fail "could not lift the whole locale block from ${script}"
|
||||
grep -q 'LANGN=.*lang_index' <<<"${block}" || fail "could not lift the whole locale block from ${script}"
|
||||
|
||||
# Run the lifted block against whatever locale vars the caller exported.
|
||||
run_block() {
|
||||
|
||||
@@ -9,4 +9,5 @@ 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.
|
||||
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" <<<"$out"
|
||||
|
||||
@@ -21,7 +21,7 @@ test "$rc" -ne 77 || {
|
||||
echo "SKIP: local crawl prerequisites missing" >&2
|
||||
exit 77
|
||||
}
|
||||
if printf '%s\n' "$out" | grep -q "could not discover server port"; then
|
||||
if grep -q "could not discover server port" <<<"$out"; 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)"
|
||||
printf '%s\n' "$out" | grep -q "watchdog fired" ||
|
||||
grep -q "watchdog fired" <<<"$out" ||
|
||||
fail "crawl failed but not via the watchdog: $out"
|
||||
|
||||
echo "crawl watchdog OK (reaped in ${elapsed}s)"
|
||||
|
||||
@@ -71,7 +71,8 @@ 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.
|
||||
grep -v "$rawframe" "$out" | grep -qE 'st_strsafe|strcpy_safe_' || {
|
||||
symbolized=$(grep -v "$rawframe" "$out" || true)
|
||||
grep -qE 'st_strsafe|strcpy_safe_' <<<"$symbolized" || {
|
||||
echo "the handler did not name the hidden frames:" >&2
|
||||
cat "$out" >&2
|
||||
exit 1
|
||||
|
||||
@@ -169,7 +169,8 @@ 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"
|
||||
|
||||
get "${port}" /server/index.html | grep -q '200 OK' ||
|
||||
reply=$(get "${port}" /server/index.html || true)
|
||||
grep -q '200 OK' <<<"$reply" ||
|
||||
fail "the server stopped answering after the refused saves"
|
||||
|
||||
echo "PASS"
|
||||
|
||||
@@ -64,6 +64,7 @@ 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 \
|
||||
@@ -227,6 +228,9 @@ TESTS = \
|
||||
103_teardown-status.test \
|
||||
104_engine-warc-longurl.test \
|
||||
105_suite-timeout.test \
|
||||
116_engine-rtrim.test
|
||||
110_local-ftp-parallel.test \
|
||||
111_local-ftp-update-rest.test \
|
||||
114_local-update-304-leak.test \
|
||||
120_local-proxytrack-webdav-default.test
|
||||
|
||||
CLEANFILES = check-network_sh.cache
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
/* Borrows Windows' rename() for 01_engine-renameover.test, since POSIX cannot
|
||||
reach hts_rename_over()'s unlink fallback: an existing target is refused with
|
||||
reach hts_rename_over()'s aside 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. */
|
||||
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). */
|
||||
|
||||
#define _GNU_SOURCE
|
||||
#include <dlfcn.h>
|
||||
@@ -18,7 +20,9 @@ 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;
|
||||
|
||||
@@ -26,7 +30,13 @@ SHIM_EXPORT int rename(const char *oldpath, const char *newpath) {
|
||||
errno = EACCES;
|
||||
return -1;
|
||||
}
|
||||
if (stat(newpath, &st) == 0) {
|
||||
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) {
|
||||
errno = EEXIST;
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,24 @@ 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 ;;
|
||||
|
||||
@@ -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")" && 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
|
||||
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
|
||||
echo PASS >"$marker"
|
||||
else
|
||||
echo "FAIL: unexpected response from \$1" >"$marker"
|
||||
|
||||
Reference in New Issue
Block a user