Compare commits

..

6 Commits

Author SHA1 Message Date
Xavier Roche
f9bf1dfde3 Drop the historical narration from hts_lastcharptr's contract
The declaration is the API surface, so it keeps the double-evaluation
warning a caller cannot derive from the signature; what the macro
replaced is git's job. Its grep exclusion in the test goes with it,
since that comment was the only htssafe.h line the scan matched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-27 14:57:26 +02:00
Xavier Roche
ce934f42d0 Merge fix/rtrim-underflow into fix/strlen-pointer-form-781 2026-07-27 14:28:51 +02:00
Xavier Roche
59b8ae7d40 Cover the collision rename on Windows and from a followed link
116_engine-rtrim ran nowhere on Windows: that job iterates a fixed glob
that 116_engine-* does not match, and the rename is savename logic, which
behaves differently there. Add it by name.

The test only drove the rename through -g, which pins depth to 0 and so
needs both colliding URLs on the command line. Under -N "%n.%t" depth is
unrestricted, so one starting URL is enough and the crawled page picks
both names itself. Also strip CR before the empty-option check, which a
CRLF log would otherwise pass vacuously.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-27 14:28:21 +02:00
Xavier Roche
d198950b8e Merge origin/master into fix/rtrim-underflow 2026-07-27 14:24:03 +02:00
Xavier Roche
4c8cb45561 x + strlen(x) - 1 points before the buffer on an empty string
The pointer spelling of #770. All 27 occurrences go through
hts_lastcharptr(), which clamps to the terminating NUL, and the two
hand-written ternary guards from #729 and #767 fold into it.

Closes #781

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-27 14:08:57 +02:00
Xavier Roche
4e77ad962b Backward scans from strlen(s) - 1 walk off the front of the buffer
optinclude_file()'s config-line right trim and url_savename()'s collision
rename both start at the last character and decrement with no lower bound.
An all-space httrackrc line and an all-digit save name walk below their
stack buffers; the second is reachable from a crawl.

Both go through hts_rtrimlen(), which counts down from the end. The
Content-Disposition trim was a third copy and now shares it.

Closes #814

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-27 14:00:35 +02:00
75 changed files with 581 additions and 1138 deletions

View File

@@ -211,8 +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 106_engine-repair-rename.test \
108_engine-refetch-backup.test; do
60_crawl-log-salvage.test 116_engine-rtrim.test; do
elapsed=$((SECONDS - started))
if [ "$elapsed" -ge "$suite_deadline" ]; then
echo "::error::suite deadline: ${elapsed}s elapsed, stopping before $t"

View File

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

View File

@@ -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 *save the server's error pages (404..) (o0 discard them) (--generate-errors)
o *generate output html file in case of error (404..) (o0 don't generate) (--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 *save the server's error pages (404..) (o0 discard them)
o *generate output html file in case of error (404..) (o0 don't generate)
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 *save the server's error pages (404..) (o0 discard them) </i></b></pre>
<pre><b><i> o *generate output html file in case of error (404..) (o0 don't generate) </i></b></pre>
<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.
<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.
<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 *save the server's error pages (404..) (o0 discard them)
o *generate output html file in case of error (404..) (o0 don't generate)
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 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
<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
unnecessary files in operations where a working web site is not the
desired result.

View File

@@ -777,8 +777,8 @@ information purpose only) (%q0 don&rsquo;t include)
<td width="82%">
<p>*save the server&rsquo;s error pages (404..) (o0 discard
them) (--generate-errors)</p></td></tr>
<p>*generate output html file in case of error (404..) (o0
don&rsquo;t generate) (--generate-errors)</p></td></tr>
<tr valign="top" align="left">
<td width="9%"></td>
<td width="4%">

View File

@@ -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 save the error pages sent by the server (if a 404 error occurred, for example)
<br><small>Do not generate error pages (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>
<!-- -->

View File

@@ -3,7 +3,7 @@
.\"
.\" This file is generated by man/makeman.sh; do not edit by hand.
.\" SPDX-License-Identifier: GPL-3.0-or-later
.TH httrack 1 "27 July 2026" "httrack website copier"
.TH httrack 1 "26 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
*save the server's error pages (404..) (o0 discard them) (\-\-generate\-errors)
*generate output html file in case of error (404..) (o0 don't generate) (\-\-generate\-errors)
.IP \-X
*purge old files after update (X0 keep delete) (\-\-purge\-old[=N])
.IP \-%p

View File

@@ -501,13 +501,11 @@ 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))
@@ -577,7 +575,6 @@ int optinclude_file(const char *name, int *argc, char **argv, char *x_argvblk,
}
}
}
}
}
fclose(fp);

View File

@@ -596,41 +596,6 @@ int back_nsoc_overall(const struct_back * sback) {
return n;
}
/* Reserved subdirectory holding a mirrored file's temporaries, beside it. */
#define HTS_TMPDIR "hts-tmp"
/* Build save's temporary as <dir>/hts-tmp/<name>.<ext>. Appending the extension
to save instead put it in the mirror namespace, so a site serving <path>.bak
had its copy taken as the backup and then unlinked (#774). HTS_FALSE (dest
emptied) if it would not fit. Note: utf-8. */
static hts_boolean back_tmpname(char *dest, size_t size, const char *save,
const char *ext) {
const char *const slash = strrchr(save, '/');
const int dirlen = slash != NULL ? (int) (slash - save) + 1 : 0;
if (!slprintfbuff(dest, size, "%.*s" HTS_TMPDIR "/%s.%s", dirlen, save,
slash != NULL ? slash + 1 : save, ext)) {
dest[0] = '\0';
return HTS_FALSE;
}
return HTS_TRUE;
}
/* Note: utf-8 */
void back_tmpdir_drop(const char *tmp) {
char BIGSTK dir[HTS_URLMAXSIZE * 2];
const char *slash;
if (tmp == NULL || (slash = strrchr(tmp, '/')) == NULL)
return;
if (!strclipbuff(dir, sizeof(dir), tmp))
return;
dir[slash - tmp] = '\0';
slash = strrchr(dir, '/');
if (strcmp(slash != NULL ? slash + 1 : dir, HTS_TMPDIR) == 0)
(void) RMDIR(dir);
}
/* generate temporary file on lien_back */
/* Note: utf-8 */
static int create_back_tmpfile(httrackp *opt, lien_back *const back,
@@ -638,10 +603,12 @@ static int create_back_tmpfile(httrackp *opt, lien_back *const back,
// do not use tempnam() but a regular filename
back->tmpfile_buffer[0] = '\0';
if (back->url_sav[0] != '\0') {
if (!back_tmpname(back->tmpfile_buffer, sizeof(back->tmpfile_buffer),
back->url_sav, ext)) {
/* same capacity as url_sav, so truncation drops the extension and aliases
the temp name onto the live file that back_finalize_backup() UNLINKs */
if (!sprintfbuff(back->tmpfile_buffer, "%s.%s", back->url_sav, ext)) {
hts_log_print(opt, LOG_WARNING, "temporary filename too long for %s",
back->url_sav);
back->tmpfile_buffer[0] = '\0';
return -1;
}
back->tmpfile = back->tmpfile_buffer;
@@ -679,11 +646,7 @@ void back_refetch_backup(httrackp *opt, lien_back *const back) {
if (fexist_utf8(back->tmpfile))
hts_log_print(opt, LOG_WARNING, "replacing leftover backup %s",
back->tmpfile);
saved = hts_rename_over(opt, back->url_sav, back->tmpfile);
/* Another slot sharing the directory may have removed it between the
structcheck above and the rename: recreate it and try once more. */
if (!saved && structcheck(back->tmpfile) == 0)
saved = hts_rename_over(opt, back->url_sav, back->tmpfile);
saved = hts_rename_over(back->url_sav, back->tmpfile);
}
if (!saved) {
hts_log_print(opt, LOG_WARNING | LOG_ERRNO,
@@ -707,22 +670,16 @@ static hts_boolean back_transfer_failed(const int statuscode) {
}
}
hts_boolean back_finalize_backup(httrackp *opt, lien_back *const back,
hts_boolean commit) {
const hts_boolean wanted = commit;
/* Commit or restore a re-fetch backup (#77 follow-up): a re-fetch over an
existing file moved the good copy to back->tmpfile before truncating url_sav.
commit keeps the new file and drops the backup; else restore it so an aborted
transfer leaves the previous copy intact. Skips the zlib .z temp. */
static void back_finalize_backup(httrackp *opt, lien_back *const back,
const hts_boolean commit) {
if (back->tmpfile == NULL || back->r.compressed)
return HTS_TRUE;
/* Nothing to commit to: filecreate() can fail after the backup was taken,
and dropping it then loses both copies (#775). */
if (commit && !fexist_utf8(back->url_sav)) {
hts_log_print(opt, LOG_WARNING, "%s was never created; restoring %s",
back->url_sav, back->tmpfile);
commit = HTS_FALSE;
}
return;
if (commit) {
(void) UNLINK(back->tmpfile); /* new copy is good; drop the backup */
back_tmpdir_drop(back->tmpfile);
} else {
if (back->r.out != NULL) {
fclose(back->r.out);
@@ -730,15 +687,12 @@ hts_boolean back_finalize_backup(httrackp *opt, lien_back *const back,
}
/* On failure keep the backup: an orphaned temp beats losing the good copy.
*/
if (!hts_rename_over(opt, back->tmpfile, back->url_sav))
if (!hts_rename_over(back->tmpfile, back->url_sav))
hts_log_print(opt, LOG_WARNING | LOG_ERRNO,
"could not restore %s; previous copy kept as %s",
back->url_sav, back->tmpfile);
else
back_tmpdir_drop(back->tmpfile);
}
back->tmpfile = NULL;
return commit == wanted ? HTS_TRUE : HTS_FALSE;
}
// objet (lien) téléchargé ou transféré depuis le cache
@@ -840,21 +794,12 @@ int back_finalize(httrackp * opt, cache_back * cache, struct_back * sback,
hts_codec_parse(back[p].r.contentencoding);
/* Never decode over url_sav: a failed decode would destroy the
copy an --update re-fetch is supposed to refresh (#557). */
char BIGSTK unpacked[HTS_URLMAXSIZE * 2];
char BIGSTK unpacked[HTS_URLMAXSIZE * 2 + 4]; // room for ".u"
LLint size;
/* fits whenever the .z temp it decodes from did */
if (!back_tmpname(unpacked, sizeof(unpacked), back[p].url_sav,
"u")) {
back[p].r.statuscode = STATUSCODE_INVALID;
strcpybuff(back[p].r.msg, "Error when decompressing (the "
"temporary filename is too long)");
/* as the decode-failure branch below: never let the coded
bytes be committed as the page */
if (!back[p].r.is_write)
deleteaddr(&back[p].r);
} else if ((size = hts_codec_unpack(codec, back[p].tmpfile,
unpacked)) >= 0) {
snprintf(unpacked, sizeof(unpacked), "%s.u", back[p].url_sav);
if ((size = hts_codec_unpack(codec, back[p].tmpfile,
unpacked)) >= 0) {
back[p].r.size = back[p].r.totalsize = size;
if (back[p].r.is_write) {
/* Sample the previous copy now: the rename below replaces
@@ -873,7 +818,7 @@ int back_finalize(httrackp * opt, cache_back * cache, struct_back * sback,
"Read error when decompressing");
}
UNLINK(unpacked);
} else if (hts_rename_over(opt, unpacked, back[p].url_sav)) {
} else if (hts_rename_over(unpacked, back[p].url_sav)) {
/* The temp bypassed filecreate(), which is what chmods. */
#ifndef _WIN32
chmod(back[p].url_sav, HTS_ACCESS_FILE);
@@ -926,7 +871,6 @@ int back_finalize(httrackp * opt, cache_back * cache, struct_back * sback,
/* ensure that no remaining temporary file exists */
if (back[p].tmpfile != NULL) {
unlink(back[p].tmpfile);
back_tmpdir_drop(back[p].tmpfile); /* the .u went with it */
back[p].tmpfile = NULL;
}
}
@@ -939,14 +883,7 @@ int back_finalize(httrackp * opt, cache_back * cache, struct_back * sback,
}
/* Body fully received: keep the freshly written url_sav, drop the
backup of the previous copy. */
if (!back_finalize_backup(opt, &back[p], HTS_TRUE)) {
/* The previous copy is back because the new one was never created;
caching this response's validators against it would pin the stale
body on every later --update. */
if (fexist_utf8(back[p].url_sav))
filenote(&opt->state.strc, back[p].url_sav, NULL);
return -1;
}
back_finalize_backup(opt, &back[p], HTS_TRUE);
/* Write mode to disk */
if (back[p].r.is_write && back[p].r.adr != NULL) {
freet(back[p].r.adr);
@@ -1782,7 +1719,6 @@ int back_clear_entry(lien_back * back) {
// only for security
if (back->tmpfile && back->tmpfile[0] != '\0') {
(void) unlink(back->tmpfile);
back_tmpdir_drop(back->tmpfile);
back->tmpfile = NULL;
}
// headers

View File

@@ -143,18 +143,6 @@ int back_finalize(httrackp * opt, cache_back * cache, struct_back * sback,
can put it back when the re-fetch fails (#77 follow-up). Call right before
truncating url_sav; tmpfile stays NULL when there is nothing to save. */
void back_refetch_backup(httrackp *opt, lien_back *const back);
/* Commit or restore a re-fetch backup (#77 follow-up): a re-fetch over an
existing file moved the good copy to back->tmpfile before truncating url_sav.
commit keeps the new file and drops the backup, unless url_sav was never
created; else restore it so an aborted transfer leaves the previous copy
intact. Skips the zlib .z temp. HTS_FALSE when a requested commit had to
restore instead: the caller then holds the OLD body and must not cache this
response's validators against it. */
hts_boolean back_finalize_backup(httrackp *opt, lien_back *const back,
hts_boolean commit);
/* Remove the reserved directory a temporary sat in, once the last slot sharing
it is done; a non-empty one just refuses. No-op outside that directory. */
void back_tmpdir_drop(const char *tmp);
/* -#test=backswap: slots eligible for the on-disk ready table. */
int back_selftest_slot_swap(void);
void back_info(struct_back * sback, int i, int j, FILE * fp);

View File

@@ -491,7 +491,7 @@ char *bauth_prefix(char *prefix, const char *adr, const char *fil) {
if (a)
*a = '\0';
if (strchr(prefix, '/')) {
a = prefix + strlen(prefix) - 1;
a = hts_lastcharptr(prefix);
while(*a != '/')
a--;
*(a + 1) = '\0';

View File

@@ -957,35 +957,6 @@ htsblk *cache_header(httrackp * opt, cache_back * cache, const char *adr,
return NULL;
}
const char *cache_repair(httrackp *opt, const char *name,
unsigned long *entries, unsigned long *bytes) {
char BIGSTK repairname[HTS_URLMAXSIZE * 2];
unzFile zip;
*entries = 0;
*bytes = 0;
if (!slprintfbuff(repairname, sizeof(repairname), "%s%s",
StringBuff(opt->path_log), "hts-cache/repair.zip"))
return "the repair path is too long";
if (unzRepair(name, repairname,
fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
StringBuff(opt->path_log), "hts-cache/repair.tmp"),
entries, bytes) != Z_OK)
return "could not repair the cache";
/* unzRepair writes an end-of-central-directory record whatever it found, so
an input holding no local file header at all yields a valid empty archive
and a short write yields a truncated one. Only an archive that holds
something and opens may replace the cache (#824). */
if (*entries == 0)
return "the repaired cache holds no entry, keeping the damaged one";
if ((zip = hts_unzOpen_utf8(repairname)) == NULL)
return "the repaired cache does not open, keeping the damaged one";
unzClose(zip);
if (!hts_rename_over(opt, repairname, name))
return "could not put the repaired cache in place";
return NULL;
}
// Initialisation du cache: créer nouveau, renomer ancien, charger..
void cache_init(cache_back * cache, httrackp * opt) {
// ---
@@ -1014,7 +985,6 @@ void cache_init(cache_back * cache, httrackp * opt) {
StringBuff(opt->path_log),
"hts-cache/new.zip")))) { // a previous cache exists.. rename it
if (!hts_rename_over(
opt,
fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
StringBuff(opt->path_log), "hts-cache/new.zip"),
fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
@@ -1055,9 +1025,8 @@ void cache_init(cache_back * cache, httrackp * opt) {
// Corrupted ZIP file ? Try to repair!
if (cache->zipInput == NULL && !cache->ro) {
char *name;
const char *why;
unsigned long repaired = 0;
unsigned long repairedBytes = 0;
uLong repaired = 0;
uLong repairedBytes = 0;
if (!cache->ro) {
name =
@@ -1070,16 +1039,25 @@ void cache_init(cache_back * cache, httrackp * opt) {
}
hts_log_print(opt, LOG_WARNING,
"Cache: damaged cache, trying to repair");
why = cache_repair(opt, name, &repaired, &repairedBytes);
if (why != NULL) {
hts_log_print(opt, LOG_WARNING | LOG_ERRNO, "Cache: %s", why);
} else if ((cache->zipInput = hts_unzOpen_utf8(name)) != NULL) {
/* mztools has no UTF-8 hook, so repairing a corrupt cache under a
non-ASCII path_log fails cleanly (re-crawl), never forks a twin. */
if (unzRepair
(name,
fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt), StringBuff(opt->path_log),
"hts-cache/repair.zip"), fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
StringBuff(opt->path_log),
"hts-cache/repair.tmp"),
&repaired, &repairedBytes) == Z_OK) {
UNLINK(name);
RENAME(fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
StringBuff(opt->path_log), "hts-cache/repair.zip"),
name);
cache->zipInput = hts_unzOpen_utf8(name);
hts_log_print(opt, LOG_WARNING,
"Cache: %d bytes successfully recovered in %d entries",
(int) repairedBytes, (int) repaired);
} else {
hts_log_print(opt, LOG_WARNING,
"Cache: the repaired cache could not be reopened");
hts_log_print(opt, LOG_WARNING, "Cache: could not repair the cache");
}
}
// Opened ?

View File

@@ -78,14 +78,6 @@ htsblk *cache_header(httrackp * opt, cache_back * cache, const char *adr,
const char *fil, htsblk * r);
void cache_init(cache_back * cache, httrackp * opt);
/* Recover the damaged cache at name into hts-cache/repair.zip and move it over
name, storing what was recovered in *entries and *bytes. Returns NULL on
success, else a reason the caller reports: a recovery that is empty or does
not open never replaces the cache, and neither does one that cannot be moved
into place (#786, #824). Note: utf-8. */
const char *cache_repair(httrackp *opt, const char *name,
unsigned long *entries, unsigned long *bytes);
/* Which hts-cache/ generation (new.* vs old.*) is authoritative. */
typedef enum {
CACHE_RECONCILE_PROMOTE, /* no new cache: promote the old generation */

View File

@@ -3774,8 +3774,7 @@ int htsAddLink(htsmoduleStruct * str, char *link) {
strcpybuff(codebase, heap(ptr)->fil);
else
strcpybuff(codebase, heap(heap(ptr)->precedent)->fil);
// empty codebase has no last char; codebase-1 would underflow
a = codebase[0] != '\0' ? codebase + strlen(codebase) - 1 : codebase;
a = hts_lastcharptr(codebase);
while((*a) && (*a != '/') && (a > codebase))
a--;
if (*a == '/')

View File

@@ -2160,9 +2160,8 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
case 'R':
{
char *name;
const char *why;
unsigned long repaired = 0;
unsigned long repairedBytes = 0;
uLong repaired = 0;
uLong repairedBytes = 0;
if (fexist_utf8(fconcat(
OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
@@ -2185,15 +2184,24 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
return 1;
}
fprintf(stderr, "Cache: trying to repair %s\n", name);
why = cache_repair(opt, name, &repaired, &repairedBytes);
if (why != NULL) {
fprintf(stderr, "Cache: %s\n", why);
return 1;
if (unzRepair
(name,
fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt), StringBuff(opt->path_log),
"hts-cache/repair.zip"),
fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt), StringBuff(opt->path_log),
"hts-cache/repair.tmp"), &repaired,
&repairedBytes) == Z_OK) {
UNLINK(name);
RENAME(fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
StringBuff(opt->path_log),
"hts-cache/repair.zip"),
name);
fprintf(stderr,
"Cache: %d bytes successfully recovered in %d entries\n",
(int) repairedBytes, (int) repaired);
} else {
fprintf(stderr, "Cache: could not repair the cache\n");
}
fprintf(
stderr,
"Cache: %d bytes successfully recovered in %d entries\n",
(int) repairedBytes, (int) repaired);
}
return 0;
break;

View File

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

View File

@@ -558,7 +558,8 @@ 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 *save the server's error pages (404..) (o0 discard them)");
infomsg
(" o *generate output html file in case of error (404..) (o0 don't generate)");
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");

View File

@@ -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, *b = NULL;
char *a = NULL;
strcpybuff(tmp, rcvd + p);
a = strstr(tmp, "filename=");
@@ -1335,15 +1335,9 @@ void treathead(t_cookie * cookie, const char *adr, const char *fil, htsblk * ret
while((c = strchr(a, '/'))) /* skip all / (see RFC2616) */
a = c + 1;
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);
}
hts_rtrim(a, HTS_SPACES);
if ((int) strlen(a) < 200) { // pas trop long?
strcpybuff(retour->cdispo, a);
}
}
}
@@ -3315,8 +3309,9 @@ int ishtml(httrackp * opt, const char *fil) {
}
/* Search for known ext */
for(a = fil_noquery + strlen(fil_noquery) - 1;
*a != '.' && *a != '/' && a > fil_noquery; a--) ;
for (a = hts_lastcharptr(fil_noquery);
*a != '.' && *a != '/' && a > fil_noquery; a--)
;
if (*a == '.') { // a une extension
char BIGSTK fil_noquery[HTS_URLMAXSIZE * 2];
char *b;
@@ -4253,9 +4248,8 @@ HTSEXT_API hts_boolean get_httptype_sized(httrackp *opt, char *s, size_t ssize,
return 1;
} else {
/* Check html -> text/html */
const char *a = fil + strlen(fil) - 1;
const char *a = hts_lastcharptr(fil);
/* a < fil when fil is empty: bound before dereferencing */
while ((a > fil) && (*a != '.') && (*a != '/'))
a--;
if (a >= fil && *a == '.' && strlen(a) < 32) {

View File

@@ -74,41 +74,6 @@ static const char *hts_tbdev[] = {
""
};
/* Directories the engine owns inside the mirror. A URL able to name one lands
on the cache or on another slot's temporary and destroys it (#774), so they
are escaped like the DOS devices are. */
static const char *hts_tbreserved[] = {"/hts-cache", "/hts-tmp", ""};
/* Replace /foo/<reserved>/bar by /foo/<reserved>_/bar, matching a whole path
component only (case-insensitively: the filesystem may be too). */
static void escapeReservedNames(char *save, size_t size,
const char *const *names) {
int i;
for (i = 0; names[i][0] != '\0'; i++) {
const char *a = save;
const size_t len = strlen(names[i]);
while ((a = strstrcase(a, names[i]))) {
switch ((int) a[len]) {
case '\0':
case '/':
case '.': {
char BIGSTK tempo[HTS_URLMAXSIZE * 2];
tempo[0] = '\0';
strncatbuff(tempo, save, (int) (a - save) + (int) len);
strcatbuff(tempo, "_");
strcatbuff(tempo, a + len);
/* clip rather than abort: the name comes from the wire */
(void) strclipbuff(save, size, tempo);
} break;
}
a += len;
}
}
}
/* Strip all // */
static void cleanDoubleSlash(char *s) {
int i, j;
@@ -858,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 = fil + strlen(fil) - 1;
char *a = hts_lastcharptr(fil);
if ((opt->debug > 1) && (opt->log != NULL)) {
if (ext_chg == 1)
@@ -893,7 +858,7 @@ int url_savename(lien_adrfilsave *const afs,
}
// Rechercher premier / et dernier .
{
const char *a = fil + strlen(fil) - 1;
const char *a = hts_lastcharptr(fil);
// passer structures
start_pos = fil;
@@ -1238,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 = fil + strlen(fil) - 1;
const char *a = hts_lastcharptr(fil);
// passer structures
while((a > fil) && (*a != '/') && (*a != '\\'))
// 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 != '.'))
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 = 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, "/");
}
if (*a != '.')
strcatbuff(afs->save, "other");
else
strcatbuff(afs->save, a + 1);
strcatbuff(afs->save, "/");
}
/*strcatbuff(save,a); */
/* add name */
ADD_STANDARD_NAME(0);
@@ -1293,7 +1258,7 @@ int url_savename(lien_adrfilsave *const afs,
}
afs->save[i + j] = '\0';
// ajouter extension
a = fil + strlen(fil) - 1;
a = hts_lastcharptr(fil);
while((a > fil) && (*a != '/') && (*a != '.'))
a--;
if (*a == '.') {
@@ -1338,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 = afs->save + strlen(afs->save) - 1;
char *a = hts_lastcharptr(afs->save);
while((a > afs->save) && (*a != '.') && (*a != '/'))
a--;
@@ -1404,12 +1369,35 @@ int url_savename(lien_adrfilsave *const afs,
// éliminer les // (comme ftp://)
cleanDoubleSlash(afs->save);
/* Runs on every platform, and before path_html is prepended below, so the
user's own output directory is never renamed. */
escapeReservedNames(afs->save, sizeof(afs->save), hts_tbreserved);
#if HTS_OVERRIDE_DOS_FOLDERS
escapeReservedNames(afs->save, sizeof(afs->save), hts_tbdev);
/* Replace /foo/nul/bar by /foo/nul_/bar */
{
int i = 0;
while(hts_tbdev[i][0]) {
const char *a = afs->save;
while((a = strstrcase(a, hts_tbdev[i]))) {
switch ((int) a[strlen(hts_tbdev[i])]) {
case '\0':
case '/':
case '.':
{
char BIGSTK tempo[HTS_URLMAXSIZE * 2];
tempo[0] = '\0';
strncatbuff(tempo, afs->save, (int) (a - afs->save) + strlen(hts_tbdev[i]));
strcatbuff(tempo, "_");
strcatbuff(tempo, a + strlen(hts_tbdev[i]));
strcpybuff(afs->save, tempo);
}
break;
}
a += strlen(hts_tbdev[i]);
}
i++;
}
}
/* Strip ending . or ' ' forbidden on windoz */
cleanEndingSpaceOrDot(afs->save);
@@ -1432,8 +1420,10 @@ int url_savename(lien_adrfilsave *const afs,
if (opt->savename_83 > 0) {
char *a, *last;
for(last = afs->save + strlen(afs->save) - 1;
last != afs->save && *last != '/' && *last != '\\' && *last != '.'; last--) ;
for (last = hts_lastcharptr(afs->save);
last != afs->save && *last != '/' && *last != '\\' && *last != '.';
last--)
;
if (*last != '.') {
last = NULL;
}
@@ -1688,8 +1678,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 = afs->save + strlen(afs->save) - 1;
char *b;
char *a = hts_lastcharptr(afs->save);
size_t stem;
int n = 2;
char collisionSeparator =
((opt->savename_83 != HTS_SAVENAME_83_ISO9660) ? '-' : '_');
@@ -1711,18 +1701,16 @@ 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)
b = tempo + strlen(tempo) - 1;
while(isdigit((unsigned char) *b))
b--;
if (*b == collisionSeparator) {
sscanf(b + 1, "%d", &n);
*b = '\0'; // couper
stem = hts_rtrimlen(tempo, "0123456789");
if (stem != 0 && tempo[stem - 1] == collisionSeparator) {
sscanf(tempo + stem, "%d", &n);
tempo[stem - 1] = '\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 = tempo + strlen(tempo) - 1;
char *a = hts_lastcharptr(tempo);
while((a > tempo) && (*a != '/'))
a--;

View File

@@ -2057,7 +2057,7 @@ int htsparse(htsmoduleStruct * str, htsmoduleStructExtended * stre) {
}
q = strchr(a, '?'); // ne pas traiter après '?'
if (!q)
q = a + strlen(a) - 1;
q = hts_lastcharptr(a);
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 = lien + strlen(lien) - 1;
char *a = hts_lastcharptr(lien);
// é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 = lien + strlen(lien) - 1;
char *a = hts_lastcharptr(lien);
while((a > lien) && (*a != '/') && (*a != '.'))
a--;
@@ -2309,7 +2309,7 @@ int htsparse(htsmoduleStruct * str, htsmoduleStructExtended * stre) {
{
char *a;
a = lien + strlen(lien) - 1;
a = hts_lastcharptr(lien);
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 = lien + strlen(lien) - 1;
if (p_type == 2) { // code ET PAS codebase
char *a = hts_lastcharptr(lien);
char *start_of_filename = jump_identification(lien);
if (start_of_filename != NULL

View File

@@ -569,6 +569,38 @@ 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

View File

@@ -6137,9 +6137,10 @@ static hts_boolean ro_is(const char *path, const char *data) {
}
// -#test=renameover <dir>: hts_rename_over() must replace an existing dst and
// never lose one it did not replace (#779, #790). Which half is live depends on
// never delete one it did not replace (#779). Which half is live depends on
// what rename() does to an existing target, so probe that and name the regime.
static int st_renameover(httrackp *opt, int argc, char **argv) {
(void) opt;
if (argc < 1) {
fprintf(stderr, "renameover: needs a writable base dir\n");
return 1;
@@ -6168,7 +6169,7 @@ static int st_renameover(httrackp *opt, int argc, char **argv) {
ro_put(dst, "old");
if (replaceable) {
/* An existing dst must still be replaced: the unlink is for this. */
if (!hts_rename_over(opt, src, dst)) {
if (!hts_rename_over(src, dst)) {
fprintf(stderr, "renameover: replacing an existing dst failed: %s\n",
strerror(errno));
err++;
@@ -6178,7 +6179,7 @@ static int st_renameover(httrackp *opt, int argc, char **argv) {
}
} else {
/* A failure the unlink cannot fix must leave dst as it was. */
if (hts_rename_over(opt, src, dst)) {
if (hts_rename_over(src, dst)) {
fprintf(stderr, "renameover: an unfixable failure reported success\n");
err++;
}
@@ -6188,36 +6189,10 @@ static int st_renameover(httrackp *opt, int argc, char **argv) {
}
}
/* A directory in the way is not something the caller asked to replace: it
must be refused, never parked aside and orphaned. */
(void) UNLINK(dst);
ro_put(src, "new");
if (MKDIR(dst) == 0) {
char parked[sizeof(dst) + 16];
snprintf(parked, sizeof(parked), "%s.hts-old0", dst);
if (hts_rename_over(opt, src, dst)) {
fprintf(stderr, "renameover: a directory at dst reported success\n");
err++;
}
if (!ro_is(src, "new")) {
fprintf(stderr, "renameover: a directory at dst consumed src\n");
err++;
}
/* RMDIR only succeeds on a directory that is there, so it doubles as the
probe: the parked name must not exist at all. */
if (RMDIR(parked) == 0 || fexist_utf8(parked)) {
fprintf(stderr, "renameover: a directory at dst was parked aside\n");
err++;
}
(void) RMDIR(dst);
}
(void) UNLINK(src);
/* A missing src must leave dst alone and report failure. */
(void) UNLINK(src);
ro_put(dst, "keep");
if (hts_rename_over(opt, src, dst)) {
if (hts_rename_over(src, dst)) {
fprintf(stderr, "renameover: a missing src reported success\n");
err++;
}
@@ -6228,177 +6203,16 @@ static int st_renameover(httrackp *opt, int argc, char **argv) {
/* Same, with dst absent too: nothing to lose, still a failure. */
(void) UNLINK(dst);
if (hts_rename_over(opt, src, dst)) {
if (hts_rename_over(src, dst)) {
fprintf(stderr, "renameover: a missing src and dst reported success\n");
err++;
}
/* The aside fallback, driven directly: a clobbering rename() never reaches
it. Skipped in the refused regime, where no rename at all succeeds. */
if (replaceable) {
char aside[sizeof(dst) + 16], keep[sizeof(dst) + 16];
snprintf(aside, sizeof(aside), "%s.hts-old0", dst);
snprintf(keep, sizeof(keep), "%s.hts-old1", dst);
(void) UNLINK(aside);
(void) UNLINK(keep);
ro_put(src, "new");
ro_put(dst, "old");
if (!hts_rename_over_aside_selftest(opt, src, dst)) {
fprintf(stderr, "renameover: the aside fallback failed: %s\n",
strerror(errno));
err++;
} else if (!ro_is(dst, "new") || fexist_utf8(src) || fexist_utf8(aside)) {
fprintf(stderr, "renameover: the aside fallback did not replace dst\n");
err++;
}
/* #790: the retry fails (no src). The old content must survive, back at dst
or, when the move back fails too, under the parked name it is logged as.
Name the outcome so a leg cannot pass having tested the other one. */
(void) UNLINK(src);
ro_put(dst, "old");
if (hts_rename_over_aside_selftest(opt, src, dst)) {
fprintf(stderr, "renameover: a failed aside retry reported success\n");
err++;
}
if (ro_is(dst, "old") && !fexist_utf8(aside)) {
printf("renameover: restore back\n");
} else if (ro_is(aside, "old") && !fexist_utf8(dst)) {
printf("renameover: restore parked\n");
(void) UNLINK(aside);
ro_put(dst, "old");
} else {
fprintf(stderr, "renameover: a failed aside retry lost the old copy\n");
err++;
}
/* An unrelated file already sitting on the aside name must survive. */
ro_put(src, "new");
ro_put(aside, "mine");
if (!hts_rename_over_aside_selftest(opt, src, dst)) {
fprintf(stderr, "renameover: a taken aside name failed the move: %s\n",
strerror(errno));
err++;
} else if (!ro_is(dst, "new") || !ro_is(aside, "mine") ||
fexist_utf8(keep)) {
fprintf(stderr, "renameover: a taken aside name was not skipped\n");
err++;
}
(void) UNLINK(aside);
(void) UNLINK(keep);
/* A directory there reads as free to the probe, so the park must skip it
on the refusal rather than give up. */
ro_put(src, "new");
ro_put(dst, "old");
if (MKDIR(aside) == 0) {
if (!hts_rename_over_aside_selftest(opt, src, dst)) {
fprintf(stderr,
"renameover: a directory on the aside name blocked the "
"move: %s\n",
strerror(errno));
err++;
} else if (!ro_is(dst, "new") || fexist_utf8(keep)) {
fprintf(stderr, "renameover: a directory on the aside name was not "
"skipped\n");
err++;
}
(void) RMDIR(aside);
}
(void) UNLINK(keep);
}
(void) UNLINK(src);
(void) UNLINK(dst);
printf("renameover: %s\n", err ? "FAIL" : "OK");
return err;
}
// -#test=refetchbackup <dir>: the #77 re-fetch backup must build its temporary
// inside the reserved hts-tmp directory, whose segment url_savename escapes so
// no mirrored file can ever sit there (#774), and must never leave the resource
// without a copy (#775).
static int st_refetchbackup(httrackp *opt, int argc, char **argv) {
lien_back *back;
char want[HTS_URLMAXSIZE * 2 + 32];
int err = 0;
if (argc < 1) {
fprintf(stderr, "refetchbackup: needs a writable base dir\n");
return 1;
}
back = calloct(1, sizeof(lien_back));
if (back == NULL) {
fprintf(stderr, "refetchbackup: out of memory\n");
return 1;
}
/* explicit separator: fconcat() joins without one, which would put the
temporary in the parent of the directory under test */
snprintf(back->url_sav, sizeof(back->url_sav), "%s/refetch.bin", argv[0]);
snprintf(want, sizeof(want), "%s/hts-tmp/refetch.bin.bak", argv[0]);
/* #774: pin the name, so moving the temporary out of the reserved directory
cannot pass without url_savename reserving wherever it went instead. */
ro_put(back->url_sav, "old");
back_refetch_backup(opt, back);
if (back->tmpfile == NULL || fexist_utf8(back->url_sav)) {
fprintf(stderr, "refetchbackup: the previous copy was not moved aside\n");
err++;
} else if (strcmp(back->tmpfile, want) != 0) {
fprintf(stderr, "refetchbackup: temporary is %s, want %s\n", back->tmpfile,
want);
err++;
}
ro_put(back->url_sav, "new"); /* what filecreate() + the transfer produce */
back_finalize_backup(opt, back, HTS_TRUE);
if (!ro_is(back->url_sav, "new")) {
fprintf(stderr, "refetchbackup: the committed copy is not the new one\n");
err++;
}
/* #758: only a killed run can leave something there, and it must be replaced
rather than disable the backup for good. */
if (structcheck(want) != 0) {
fprintf(stderr, "refetchbackup: cannot create %s\n", want);
freet(back);
return 1;
}
ro_put(want, "leftover");
back_refetch_backup(opt, back);
if (back->tmpfile == NULL || !ro_is(want, "new")) {
fprintf(stderr, "refetchbackup: a leftover temporary blocked the backup\n");
err++;
}
/* #775: filecreate() failed, so there is nothing to commit to. Saying so is
load-bearing: the caller must not cache this response against the old
body, or the next --update gets a 304 pinning it. */
(void) UNLINK(back->url_sav);
if (back_finalize_backup(opt, back, HTS_TRUE)) {
fprintf(stderr, "refetchbackup: a commit that restored reported success\n");
err++;
}
if (!ro_is(back->url_sav, "new")) {
fprintf(stderr, "refetchbackup: a commit with no new copy lost both\n");
err++;
}
/* An aborted transfer restores, as before. */
back_refetch_backup(opt, back);
ro_put(back->url_sav, "partial");
back_finalize_backup(opt, back, HTS_FALSE);
if (!ro_is(back->url_sav, "new")) {
fprintf(stderr, "refetchbackup: an aborted re-fetch kept the partial\n");
err++;
}
(void) UNLINK(back->url_sav);
freet(back);
printf("refetchbackup: %s\n", err ? "FAIL" : "OK");
return err;
}
// -#test=direnum <dir>: enumerate a long+non-ASCII directory via the
// opendir/readdir wrappers; children must round-trip as UTF-8 (#133,#630).
static int st_direnum(httrackp *opt, int argc, char **argv) {
@@ -6896,9 +6710,10 @@ static int st_changes_race(httrackp *opt, int argc, char **argv) {
return err;
}
/* 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. */
/* 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. */
static int st_lastchar(httrackp *opt, int argc, char **argv) {
enum { off = 8 };
@@ -6965,11 +6780,52 @@ 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
@@ -6978,6 +6834,91 @@ 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. */
/* ------------------------------------------------------------ */
@@ -7033,8 +6974,10 @@ 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)",
{"lastchar", "",
"last-char helpers never index before the buffer (#770, #781)",
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)",
@@ -7149,9 +7092,6 @@ static const struct selftest_entry {
"hts_rename_over(): replace dst, but never delete a dst it did not "
"replace",
st_renameover},
{"refetchbackup", "<dir>",
"the re-fetch backup always leaves a copy, and stays out of the mirror",
st_refetchbackup},
{"direnum", "<dir>",
"enumerate a long+non-ASCII directory through opendir/readdir",
st_direnum},

View File

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

View File

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

View File

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

View File

@@ -38,7 +38,6 @@ Please visit our Website: http://www.httrack.com
#include "htscore.h"
#include "htslib.h"
#include "htsback.h"
#include "htstools.h"
#include "htssafe.h"
#include "htszlib.h"
@@ -1115,7 +1114,7 @@ static void warc_wacz_package(warc_writer *w) {
hts_log_print(w->opt, LOG_WARNING,
"WACZ: packaging failed, kept existing %s untouched",
waczpath);
} else if (!hts_rename_over(w->opt, tmppath, waczpath)) {
} else if (!hts_rename_over(tmppath, waczpath)) {
(void) UNLINK(fconv(catbuff, sizeof(catbuff), tmppath));
hts_log_print(w->opt, LOG_WARNING | LOG_ERRNO,
"WACZ: could not finalize %s", waczpath);
@@ -1355,7 +1354,6 @@ void warc_free_request(htsblk *r) {
freet(r->warc_resphdr);
if (r->warc_rawpath != NULL) {
(void) UNLINK(r->warc_rawpath); /* owns the verbatim spool file */
back_tmpdir_drop(r->warc_rawpath);
freet(r->warc_rawpath);
r->warc_rawpath = NULL;
}
@@ -1591,7 +1589,7 @@ static hts_boolean warc_commit(warc_writer *w) {
for (s = 0; s < nseg; s++) {
const char *final = warc_seg_path(w, s, finalbuf, sizeof(finalbuf));
snprintf(tmpbuf, sizeof(tmpbuf), "%s" WARC_TMP_SUFFIX, final);
if (!hts_rename_over(w->opt, tmpbuf, final)) {
if (!hts_rename_over(tmpbuf, final)) {
hts_log_print(w->opt, LOG_ERROR | LOG_ERRNO,
"WARC: could not replace %s", final);
return HTS_FALSE;

View File

@@ -903,10 +903,11 @@ 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 = filename + (int) strlen(filename) - 1;
abpath > filename && ((*abpath != '/' && *abpath != '\\')
|| --slashes > 0);
abpath--) ;
for (slashes = 2, abpath = hts_lastcharptr(filename);
abpath > filename &&
((*abpath != '/' && *abpath != '\\') || --slashes > 0);
abpath--)
;
index->path[0] = '\0';
if (slashes == 0 && *abpath != 0) {
int i;
@@ -1499,10 +1500,11 @@ 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 = filename + (int) strlen(filename) - 1;
abpath > filename && ((*abpath != '/' && *abpath != '\\')
|| --slashes > 0);
abpath--) ;
for (slashes = 2, abpath = hts_lastcharptr(filename);
abpath > filename &&
((*abpath != '/' && *abpath != '\\') || --slashes > 0);
abpath--)
;
index->path[0] = '\0';
if (slashes == 0 && *abpath != 0) {
int i;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
#!/bin/bash
#
# 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.
# 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.
set -eu
@@ -33,3 +33,36 @@ 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

View File

@@ -4,5 +4,4 @@
set -euo pipefail
out=$(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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -165,20 +165,3 @@ MINGW* | MSYS* | CYGWIN*)
name "/$(printf 'a%.0s' {1..300}).php" 'text/html' "$(printf 'a%.0s' {1..300}).html"
;;
esac
# #774: the engine owns hts-cache/ and hts-tmp/ inside the mirror, so a URL must
# never be able to name one; a site serving them gets the /nul -> nul_ treatment.
full '/hts-tmp/a.bin' 'application/octet-stream' \
'/dev/null/www.example.com/hts-tmp_/a.bin'
full '/hts-cache/new.zip' 'application/octet-stream' \
'/dev/null/www.example.com/hts-cache_/new.zip'
full '/d/hts-tmp/a.bin.bak' 'application/octet-stream' \
'/dev/null/www.example.com/d/hts-tmp_/a.bin.bak'
# A whole component only, and case-insensitively: the filesystem may be too.
full '/hts-tmpfoo/a.bin' 'application/octet-stream' \
'/dev/null/www.example.com/hts-tmpfoo/a.bin'
full '/HTS-TMP/a.bin' 'application/octet-stream' \
'/dev/null/www.example.com/HTS-TMP_/a.bin'
# Control: the DOS device names this reuses must still be escaped.
full '/nul/x.bin' 'application/octet-stream' \
'/dev/null/www.example.com/nul_/x.bin'

View File

@@ -7,11 +7,11 @@ set -eu
# Bare -#test lists known tests (printed to stderr).
list=$(httrack -#test 2>&1)
grep -q "filter" <<<"$list" || exit 1
grep -q "cache-writefail" <<<"$list" || exit 1
printf '%s\n' "$list" | grep -q "filter" || exit 1
printf '%s\n' "$list" | grep -q "cache-writefail" || exit 1
# Unknown name: non-zero exit + diagnostic, and no test result line.
rc=0
err=$(httrack -#test=bogus 2>&1) || rc=$?
test "$rc" -ne 0 || exit 1
grep -q "Unknown self-test" <<<"$err" || exit 1
printf '%s\n' "$err" | grep -q "Unknown self-test" || exit 1

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,8 @@
#!/bin/bash
#
# Stick to POSIX tool flags: macOS ships BSD grep/sed, not the GNU ones.
# Keep this POSIX-portable: the harness runs it via $(BASH), which is a plain
# POSIX /bin/sh on some platforms (e.g. macOS), so avoid bashisms and GNU-only
# tool flags despite the #!/bin/bash above.
# Cache write-failure policy (-#test=cache-writefail <dir>). #174/#219: disk
# full or a failure streak aborts cleanly; an isolated failure or an oversized
@@ -15,13 +17,13 @@ out=$(httrack -#test=cache-writefail "$dir")
# Match the exact success line (error logs also go to stdout); a renamed/removed
# test prints the registry to stderr, which exits non-zero but never prints this.
grep -qx "cache-writefail: OK" <<<"$out" || {
printf '%s\n' "$out" | grep -qx "cache-writefail: OK" || {
echo "expected 'cache-writefail: OK', got: $out" >&2
exit 1
}
# A skipped entry must be warned about with its URL.
grep -q "entry not cached: example.com/" <<<"$out" || {
printf '%s\n' "$out" | grep -q "entry not cached: example.com/" || {
echo "expected a URL-bearing skip warning" >&2
exit 1
}

View File

@@ -1,6 +1,8 @@
#!/bin/bash
#
# Stick to POSIX tool flags: macOS ships BSD grep/sed, not the GNU ones.
# Keep this POSIX-portable: the harness runs it via $(BASH), which is a plain
# POSIX /bin/sh on some platforms (e.g. macOS), so avoid bashisms and GNU-only
# tool flags despite the #!/bin/bash above.
# unzRepair header read must not overflow a signed shift (-#test=zip-repair-shift
# <dir>). A damaged local file header whose CRC high word has bit 15 set made
@@ -13,7 +15,7 @@ trap 'set +e; rm -rf "$dir"' EXIT
out=$(httrack -#test=zip-repair-shift "$dir")
grep -qx "zip-repair-shift: OK (recovered 1 entry)" <<<"$out" || {
printf '%s\n' "$out" | grep -qx "zip-repair-shift: OK (recovered 1 entry)" || {
echo "expected 'zip-repair-shift: OK (recovered 1 entry)', got: $out" >&2
exit 1
}

View File

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

View File

@@ -12,8 +12,8 @@ set -euo pipefail
: "${top_srcdir:=..}"
bash "$top_srcdir/tests/local-crawl.sh" \
--plant-file bigtrunc/hts-tmp/slow.bin.bak \
--plant-dir bigtrunc/hts-tmp/fast.bin.bak \
--plant-file bigtrunc/slow.bin.bak \
--plant-dir bigtrunc/fast.bin.bak \
--rerun-args '--update -M400000' \
--log-found 'More than 400000 bytes have been transferred.. giving up' \
--log-found 'replacing leftover backup .*slow\.bin\.bak' \

View File

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

View File

@@ -1,173 +0,0 @@
#!/bin/bash
#
# #786: both cache-repair paths moved the recovered zip onto the cache without
# checking the move, and announced success either way. #824: they also committed
# a recovery holding no entry at all. Neither may replace the cache, and both
# must be reported.
set -euo pipefail
testdir=$(cd "$(dirname "$0")" && pwd)
# shellcheck source=tests/testlib.sh
. "${testdir}/testlib.sh"
tmpdir=$(mktemp -d) # honors the Windows-shaped TMPDIR the MSYS suite exports
cleanup() { rm -rf "$tmpdir"; }
trap 'set +e; cleanup' EXIT
trap 'set +e; cleanup; exit 1' HUP INT QUIT PIPE TERM
# One local file header, no central directory: unzOpen fails, unzRepair
# recovers the single entry. Same vector as -#test=zip-repair-shift.
plant_damaged_cache() {
rm -rf "$1"
mkdir -p "$1/hts-cache"
printf '\120\113\003\004\024\000\000\000\000\000\000\000\000\000\000\000\350\212\000\000\000\000\000\000\000\000\001\000\000\000\141' \
>"$1/hts-cache/new.zip"
}
dead=http://127.0.0.1:1/
# --- the CLI path (-#R), repair succeeding -----------------------------------
printf '[-#R repairs in place] ..\t'
proj="${tmpdir}/cli"
plant_damaged_cache "$proj"
out=$(httrack -O "$proj" -#R "$dead" 2>&1)
grep -q 'successfully recovered' <<<"$out" || {
echo "FAIL: no recovery reported: $out"
exit 1
}
test ! -e "${proj}/hts-cache/repair.zip" || {
echo "FAIL: the recovery was left in repair.zip"
exit 1
}
test "$(wc -c <"${proj}/hts-cache/new.zip")" -gt 31 || {
echo "FAIL: new.zip is still the damaged one"
exit 1
}
echo "OK"
# --- #824: nothing was recoverable, so nothing may be committed --------------
printf '[-#R refuses an empty recovery] ..\t'
proj="${tmpdir}/empty"
rm -rf "$proj"
mkdir -p "$proj/hts-cache"
printf 'not a zip at all, some bytes' >"$proj/hts-cache/new.zip"
rc=0
out=$(httrack -O "$proj" -#R "$dead" 2>&1) || rc=$?
test "$rc" -ne 0 || {
echo "FAIL: an empty recovery exited 0: $out"
exit 1
}
if grep -q 'successfully recovered' <<<"$out"; then
echo "FAIL: an empty recovery claimed success: $out"
exit 1
fi
test "$(wc -c <"${proj}/hts-cache/new.zip")" -eq 28 || {
echo "FAIL: the damaged cache was replaced by the empty recovery"
exit 1
}
echo "OK"
if [ "$(uname -s)" != "Linux" ]; then
echo "repair-rename: LD_PRELOAD interposition is Linux-only here, skipping"
exit 0
fi
# A --disable-shared build has nothing to preload; anything else missing is a
# build problem, not a skip, or the failure legs would pass vacuously.
if [ ! -r "${RENAMEFAIL_LA:-}" ]; then
echo "repair-rename: ${RENAMEFAIL_LA:-\$RENAMEFAIL_LA} was not built" >&2
exit 1
fi
if grep -q "^dlname=''" "$RENAMEFAIL_LA"; then
echo "repair-rename: static-only build, skipping the interposed legs"
exit 0
fi
if [ ! -r "${RENAMEFAIL_LIB:-}" ]; then
echo "repair-rename: ${RENAMEFAIL_LIB:-\$RENAMEFAIL_LIB} was not built" >&2
exit 1
fi
# An LD_PRELOAD library loads ahead of the executable's own libasan, which ASan
# refuses by default. The shim allocates nothing, so the ordering is harmless.
export ASAN_OPTIONS="${ASAN_OPTIONS:+$ASAN_OPTIONS:}verify_asan_link_order=0"
export RENAMEFAIL_MODE=repair
export LD_PRELOAD="$RENAMEFAIL_LIB"
# --- the CLI path, move refused ----------------------------------------------
printf '[-#R reports a refused move] ..\t'
proj="${tmpdir}/cli-fail"
plant_damaged_cache "$proj"
rc=0
out=$(httrack -O "$proj" -#R "$dead" 2>&1) || rc=$?
test "$rc" -ne 0 || {
echo "FAIL: a refused move exited 0: $out"
exit 1
}
if grep -q 'successfully recovered' <<<"$out"; then
echo "FAIL: a refused move still claimed success: $out"
exit 1
fi
test -s "${proj}/hts-cache/repair.zip" || {
echo "FAIL: the recovery was not kept for a retry"
exit 1
}
test "$(wc -c <"${proj}/hts-cache/new.zip")" -eq 31 || {
echo "FAIL: the damaged cache was destroyed anyway"
exit 1
}
echo "OK"
# --- the engine path (cache_init), move refused ------------------------------
printf '[cache_init reports a refused move] ..\t'
proj="${tmpdir}/engine"
plant_damaged_cache "$proj"
httrack -O "$proj" "$dead" --timeout=2 --retries=0 -q >/dev/null 2>&1 || true
log="${proj}/hts-log.txt"
test -r "$log" || {
echo "FAIL: no crawl log"
exit 1
}
grep -q 'damaged cache' "$log" || {
echo "FAIL: the repair path never ran"
exit 1
}
grep -q 'could not put the repaired cache in place' "$log" || {
echo "FAIL: a refused move was not reported: $(grep -i cache "$log")"
exit 1
}
if grep -q 'successfully recovered' "$log"; then
echo "FAIL: a refused move still claimed success"
exit 1
fi
test -s "${proj}/hts-cache/repair.zip" || {
echo "FAIL: the recovery was not kept for a retry"
exit 1
}
echo "OK"
# --- the move is refused the way Windows refuses it --------------------------
# The destination always exists there, so every repair takes hts_rename_over()'s
# fallback. Before #790 that fallback removed the cache and then failed.
printf '[a refused fallback keeps the cache] ..\t'
proj="${tmpdir}/eexist"
plant_damaged_cache "$proj"
rc=0
out=$(RENAMEFAIL_MODE=repair-eexist httrack -O "$proj" -#R "$dead" 2>&1) || rc=$?
test "$rc" -ne 0 || {
echo "FAIL: a refused fallback exited 0: $out"
exit 1
}
test "$(wc -c <"${proj}/hts-cache/new.zip")" -eq 31 || {
echo "FAIL: the fallback destroyed the cache"
exit 1
}
test -s "${proj}/hts-cache/repair.zip" || {
echo "FAIL: the recovery was not kept for a retry"
exit 1
}
leftover=$(find "${proj}/hts-cache" -name '*.hts-old*' | head -1)
test -z "$leftover" || {
echo "FAIL: the parked copy was left behind: $leftover"
exit 1
}
echo "OK"

View File

@@ -1,21 +0,0 @@
#!/bin/bash
#
# #774: the re-fetch backup lives in an hts-tmp directory beside the mirrored
# file, so the site here serves that exact path. url_savename must escape the
# reserved segment, landing the sibling in hts-tmp_ where the backup of a.bin
# cannot consume it. Asserting the unescaped path instead would pass with the
# bug live, which is how the first attempt at this test missed it.
set -euo pipefail
: "${top_srcdir:=..}"
bash "$top_srcdir/tests/local-crawl.sh" \
--rerun-args '--update' \
--file-matches 'bakname/a.bin' 'BAKNAME-MAIN-V2' \
--file-matches 'bakname/hts-tmp_/a.bin.bak' 'BAKNAME-SIBLING' \
--file-min-bytes 'bakname/hts-tmp_/a.bin.bak' 2048 \
--not-found 'bakname/hts-tmp/a.bin.bak' \
--log-not-found 'could not back up' \
--log-not-found 'could not restore' \
httrack 'BASEURL/bakname/index.html'

View File

@@ -1,25 +0,0 @@
#!/bin/bash
#
# Drives -#test=refetchbackup: the #77 re-fetch backup must leave a copy of the
# resource whatever happens, and must not build its temporary name inside the
# mirror namespace (#774, #775).
set -euo pipefail
dir=$(mktemp -d)
trap 'set +e; rm -rf "$dir"' EXIT
trap 'set +e; rm -rf "$dir"; exit 1' HUP INT QUIT PIPE TERM
rc=0
out=$(httrack -O /dev/null -#test=refetchbackup "$dir" 2>&1) || rc=$?
if test "$rc" -ne 0 || ! grep -q "refetchbackup: OK" <<<"$out"; then
echo "FAIL (exit $rc): $out" >&2
exit 1
fi
# The temporaries live in hts-tmp and the last user removes it.
leftovers=$(find "$dir" -mindepth 1 | head -5)
test -z "$leftovers" || {
echo "FAIL: leftover temporaries: $leftovers" >&2
exit 1
}

View File

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

107
tests/116_engine-rtrim.test Executable file
View File

@@ -0,0 +1,107 @@
#!/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

View File

@@ -13,10 +13,7 @@ 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 body is the server's verbatim, no HTTrack marker (#787); good.html proves the marker check can fire.
# Control -o1 (default): the 404 error page is written.
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'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -227,9 +227,6 @@ TESTS = \
103_teardown-status.test \
104_engine-warc-longurl.test \
105_suite-timeout.test \
106_engine-repair-rename.test \
107_local-bak-collision.test \
108_engine-refetch-backup.test \
111_local-ftp-update-rest.test
116_engine-rtrim.test
CLEANFILES = check-network_sh.cache

View File

@@ -345,7 +345,6 @@ if test "${#plants[@]}" -gt 0; then
if test "${plants[$i]}" = "--plant-dir"; then
mkdir -p "$path" || die "could not create $path"
else
mkdir -p "$(dirname "$path")" || die "could not create ${path%/*}"
echo "$plant_poison" >"$path" || die "could not write $path"
fi
result "OK"
@@ -530,25 +529,9 @@ if test -n "$wacz_validate"; then
fi
# No crawl, even a cancelled one, may leave engine temporaries: .delayed (#107,
# #483), the .z/.u content-coding temps (#557), or the hts-tmp directory those
# and the re-fetch backup live in (#774). Only a test that planted something in
# hts-tmp itself owns what is left there, so only that case skips the scan.
# #483), or the .z/.u content-coding temps (#557).
info "checking for leftover engine temporaries"
scan_tmpdir=1
i=0
while test "$i" -lt "${#plants[@]}"; do
case "${plants[$((i + 1))]}" in
*/hts-tmp/*) scan_tmpdir=0 ;;
esac
i=$((i + 2))
done
if test "$scan_tmpdir" -eq 1; then
leftovers=$(find "$out" \( -name '*.delayed' -o -name '*.z' -o -name '*.u' \
-o -name 'hts-tmp' \) 2>/dev/null | head -5)
else
leftovers=$(find "$out" \( -name '*.delayed' -o -name '*.z' -o -name '*.u' \) \
2>/dev/null | head -5)
fi
leftovers=$(find "$out" \( -name '*.delayed' -o -name '*.z' -o -name '*.u' \) 2>/dev/null | head -5)
if test -z "$leftovers"; then result "OK"; else
result "leftover: $leftovers"
exit 1

View File

@@ -1553,40 +1553,6 @@ class Handler(SimpleHTTPRequestHandler):
def route_mini304_page(self):
self.big_send(b"<html><body>tiny cacheable page</body></html>\n", "text/html")
# --- /bakname/: #774 — a mirrored file named like a re-fetch backup ----
# a.bin gets a new body every pass, so the update re-fetches it and takes a
# backup. The sibling carries a validator, so it only revalidates and must
# still be there afterwards. It is served under hts-tmp/, the directory the
# backup lives in, so the crawl asks for the exact path the engine writes.
BAKNAME_SIB = b"BAKNAME-SIBLING\n" + b"\x41\x42\x43\x44" * 512
def route_bakname_index(self):
self.send_html(
'\t<a href="a.bin">a</a>\n'
'\t<a href="hts-tmp/a.bin.bak">bak</a>\n'
)
def route_bakname_main(self):
v = 1 if self.refetch_pass() == 1 else 2
self.send_raw(
b"BAKNAME-MAIN-V%d\n" % v + b"\x31\x32\x33\x34" * 512,
"application/octet-stream",
)
def route_bakname_sibling(self):
if self.headers.get("If-Modified-Since") or self.headers.get("If-None-Match"):
self.send_response(304)
self.send_header("Content-Length", "0")
self.end_headers()
return
self.send_response(200)
self.send_header("Content-Type", "application/octet-stream")
self.send_header("Last-Modified", BIG_LASTMOD)
self.send_header("Content-Length", str(len(self.BAKNAME_SIB)))
self.end_headers()
if self.command != "HEAD":
self.wfile.write(self.BAKNAME_SIB)
# --- /errmask/: issue #176 — a page that 200'd on the first crawl but 403s
# on the update fetch must keep its good copy, not be overwritten nor purged.
ERRMASK_GOOD = b"KEEP" + b"." * 1020 # 1024 B distinctive non-HTML body
@@ -2168,9 +2134,6 @@ class Handler(SimpleHTTPRequestHandler):
"/redir/index.html": route_redir_index,
"/redir/go.php": route_redir_go,
"/redir/target.html": route_redir_target,
"/bakname/index.html": route_bakname_index,
"/bakname/a.bin": route_bakname_main,
"/bakname/hts-tmp/a.bin.bak": route_bakname_sibling,
"/mini304/index.html": route_mini304_index,
"/mini304/page.html": route_mini304_page,
"/errmask/index.html": route_errmask_index,

View File

@@ -1,10 +1,7 @@
/* Borrows Windows' rename() for the tests POSIX cannot otherwise reach:
hts_rename_over()'s aside fallback needs an existing target refused with
/* Borrows Windows' rename() for 01_engine-renameover.test, since POSIX cannot
reach hts_rename_over()'s unlink fallback: an existing target is refused with
EEXIST, and RENAMEFAIL_MODE=locked reports EACCES instead, as the CRT does
for a source another process holds. =repair and =repair-eexist narrow the
refusal to the cache-repair move (#786), the latter through the fallback so
the retry inside it is the failing one. RENAMEFAIL_ASIDE_FAILS=N refuses the
first N moves back out of a parked ".hts-old" name (#790). */
for a source another process holds. */
#define _GNU_SOURCE
#include <dlfcn.h>
@@ -21,32 +18,15 @@ 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 char *const slash = strrchr(oldpath, '/');
const char *const base = slash != NULL ? slash + 1 : oldpath;
const int repaired = strcmp(base, "repair.zip") == 0;
const int locked = mode != NULL && strcmp(mode, "locked") == 0;
struct stat st;
if (mode != NULL && (strcmp(mode, "locked") == 0 ||
(repaired && strcmp(mode, "repair") == 0))) {
if (locked) {
errno = EACCES;
return -1;
}
if (repaired && mode != NULL && strcmp(mode, "repair-eexist") == 0) {
errno = stat(newpath, &st) == 0 ? EEXIST : EACCES;
return -1;
}
if (aside_fails != NULL && strstr(base, ".hts-old") != NULL &&
aside_failures < atoi(aside_fails)) {
aside_failures++;
errno = EACCES;
return -1;
}
/* Only the bare shim emulates Windows for every rename; a narrowed mode
leaves the rest of the run on plain POSIX semantics. */
if (mode == NULL && aside_fails == NULL && stat(newpath, &st) == 0) {
if (stat(newpath, &st) == 0) {
errno = EEXIST;
return -1;
}

View File

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