mirror of
https://github.com/xroche/httrack.git
synced 2026-07-27 19:12:54 +03:00
Compare commits
21 Commits
fix/strlen
...
fix/refetc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fbdc19126 | ||
|
|
c31f53132f | ||
|
|
1b3f7918b6 | ||
|
|
aeef177267 | ||
|
|
0984aa2530 | ||
|
|
bc6c53598c | ||
|
|
809d5b6ffc | ||
|
|
46bc313469 | ||
|
|
4446923d89 | ||
|
|
f39270bdd6 | ||
|
|
ea6aafb210 | ||
|
|
8bad42978a | ||
|
|
12775c2509 | ||
|
|
57098870d3 | ||
|
|
3ed88825f7 | ||
|
|
a61178bba7 | ||
|
|
6be18f430e | ||
|
|
38161162e2 | ||
|
|
8c7e0edc4e | ||
|
|
28c103db70 | ||
|
|
8cf32966fa |
3
.github/workflows/windows-build.yml
vendored
3
.github/workflows/windows-build.yml
vendored
@@ -211,7 +211,8 @@ jobs:
|
||||
pass=0 fail=0 skip=0 failed="" skipped="" deadline=0
|
||||
for t in 00_runnable.test 01_engine-*.test 01_zlib-*.test \
|
||||
*_local-*.test 13_crawl_proxy_https.test 58_watchdog.test \
|
||||
60_crawl-log-salvage.test 116_engine-rtrim.test; do
|
||||
60_crawl-log-salvage.test 106_engine-repair-rename.test \
|
||||
108_engine-refetch-backup.test; do
|
||||
elapsed=$((SECONDS - started))
|
||||
if [ "$elapsed" -ge "$suite_deadline" ]; then
|
||||
echo "::error::suite deadline: ${elapsed}s elapsed, stopping before $t"
|
||||
|
||||
14
AGENTS.md
14
AGENTS.md
@@ -30,12 +30,14 @@ the operational checklist: toolchain, invariants, and how to ship a change.
|
||||
failing cleanup command becomes the test's exit status (#773). Keep the other
|
||||
signals on their own `trap` line, or errexit stays off for the rest of the run.
|
||||
The guard also resets `$?`, so save it first if teardown reads it.
|
||||
- Never assert with `cmd | grep -q MARKER && fail`. Under `pipefail` the
|
||||
pipeline is non-zero both when `cmd` fails and when `grep -q` matches early
|
||||
and SIGPIPEs it, so the `&&` never fires and a probe that proved nothing reads
|
||||
as "marker absent". Capture the reply, assert the status line it must carry
|
||||
(an empty, truncated or redirected one is marker-free too), then match with a
|
||||
here-string.
|
||||
- Never pipe into `grep -q`: it exits on the first match, so whatever the
|
||||
producer had left to write takes SIGPIPE, and under `pipefail` that becomes
|
||||
the pipeline's status. `cmd | grep -q M && fail` then never fires and a probe
|
||||
that proved nothing reads as "marker absent"; `cmd | grep -q M || fail` fails
|
||||
a test whose marker was present. bash issues one `write()` per line, so any
|
||||
match that is not on the last line is exposed. Capture the reply, assert the
|
||||
status line it must carry (an empty, truncated or redirected one is
|
||||
marker-free too), then match with a here-string: `grep -q M <<<"$reply"`.
|
||||
|
||||
## Hard invariants
|
||||
- **Generated autotools files are NOT in git.** `configure`, every
|
||||
|
||||
@@ -237,7 +237,7 @@ Build options:
|
||||
x replace external html links by error pages (--replace-external)
|
||||
%x do not include any password for external password protected websites (%x0 include) (--no-passwords)
|
||||
%q *include query string for local files (useless, for information purpose only) (%q0 don't include) (--include-query-string)
|
||||
o *generate output html file in case of error (404..) (o0 don't generate) (--generate-errors)
|
||||
o *save the server's error pages (404..) (o0 discard them) (--generate-errors)
|
||||
X *purge old files after update (X0 keep delete) (--purge-old[=N])
|
||||
|
||||
Spider options:
|
||||
@@ -413,7 +413,7 @@ site. Specifically, the defauls are:
|
||||
NN name conversion type (0 *original structure, 1+: see below)
|
||||
LN long names (L1 *long names / L0 8-3 conversion)
|
||||
K keep original links (e.g. http://www.adr/link) (K0 *relative link)
|
||||
o *generate output html file in case of error (404..) (o0 don't generate)
|
||||
o *save the server's error pages (404..) (o0 discard them)
|
||||
X *purge old files after update (X0 keep delete)
|
||||
bN accept cookies in cookies.txt (0=do not accept,* 1=accept)
|
||||
u check document type if unknown (cgi,asp..) (u0 don't check, * u1 check but /, u2 check always)
|
||||
@@ -473,11 +473,11 @@ store them with the same names used on the web site.
|
||||
URLs within this web site are adjusted to point to the files in the
|
||||
mirror.
|
||||
|
||||
<pre><b><i> o *generate output html file in case of error (404..) (o0 don't generate) </i></b></pre>
|
||||
<pre><b><i> o *save the server's error pages (404..) (o0 discard them) </i></b></pre>
|
||||
|
||||
<p align=justify> IF there are errors in downloading, create a file that
|
||||
indicates that the URL was not found. This makes browsing go a lot
|
||||
smoother.
|
||||
<p align=justify> IF a page cannot be downloaded, the error page the
|
||||
server sent is saved in its place, so a broken link still lands on the
|
||||
site's own 'not found' page. This makes browsing go a lot smoother.
|
||||
|
||||
<pre><b><i> X *purge old files after update (X0 keep delete) </i></b></pre>
|
||||
|
||||
@@ -1011,7 +1011,7 @@ Build options:
|
||||
LN long names (L1 *long names / L0 8-3 conversion)
|
||||
K keep original links (e.g. http://www.adr/link) (K0 *relative link)
|
||||
x replace external html links by error pages
|
||||
o *generate output html file in case of error (404..) (o0 don't generate)
|
||||
o *save the server's error pages (404..) (o0 discard them)
|
||||
X *purge old files after update (X0 keep delete)
|
||||
%x do not include any password for external password protected websites (%x0 include) (--no-passwords)
|
||||
%q *include query string for local files (information only) (%q0 don't include) (--include-query-string)
|
||||
@@ -1118,9 +1118,9 @@ deactivated byt his process.
|
||||
httrack http://www.shoesizes.com -O /tmp/shoesizes -x
|
||||
</i></b></pre>
|
||||
|
||||
<p align=justify> This option prevents the generation of '404' error
|
||||
files to replace files that were not found even though there were URLs
|
||||
pointing to them. It is useful for saving space as well as eliminating
|
||||
<p align=justify> This option keeps the server's '404' error pages out
|
||||
of the mirror, even though there were URLs pointing to the missing
|
||||
files. It is useful for saving space as well as eliminating
|
||||
unnecessary files in operations where a working web site is not the
|
||||
desired result.
|
||||
|
||||
|
||||
@@ -777,8 +777,8 @@ information purpose only) (%q0 don’t include)
|
||||
<td width="82%">
|
||||
|
||||
|
||||
<p>*generate output html file in case of error (404..) (o0
|
||||
don’t generate) (--generate-errors)</p></td></tr>
|
||||
<p>*save the server’s error pages (404..) (o0 discard
|
||||
them) (--generate-errors)</p></td></tr>
|
||||
<tr valign="top" align="left">
|
||||
<td width="9%"></td>
|
||||
<td width="4%">
|
||||
|
||||
@@ -122,7 +122,7 @@ h4 { margin: 0; font-weight: bold; font-size: 1.18em; }
|
||||
</small><br><br>
|
||||
<!-- -->
|
||||
<li>No error pages</li>
|
||||
<br><small>Do not generate error pages (if a 404 error occurred, for example)
|
||||
<br><small>Do not save the error pages sent by the server (if a 404 error occurred, for example)
|
||||
<br>If a page is missing on the remote site, there will not be any warning on the local site
|
||||
</small><br><br>
|
||||
<!-- -->
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
.\"
|
||||
.\" This file is generated by man/makeman.sh; do not edit by hand.
|
||||
.\" SPDX-License-Identifier: GPL-3.0-or-later
|
||||
.TH httrack 1 "26 July 2026" "httrack website copier"
|
||||
.TH httrack 1 "27 July 2026" "httrack website copier"
|
||||
.SH NAME
|
||||
httrack \- offline browser : copy websites to a local directory
|
||||
.SH SYNOPSIS
|
||||
@@ -220,7 +220,7 @@ do not include any password for external password protected websites (%x0 includ
|
||||
.IP \-%g
|
||||
strip query keys for dedup ([host/pattern=]key1,key2,...) (\-\-strip\-query <param>)
|
||||
.IP \-o
|
||||
*generate output html file in case of error (404..) (o0 don't generate) (\-\-generate\-errors)
|
||||
*save the server's error pages (404..) (o0 discard them) (\-\-generate\-errors)
|
||||
.IP \-X
|
||||
*purge old files after update (X0 keep delete) (\-\-purge\-old[=N])
|
||||
.IP \-%p
|
||||
|
||||
@@ -501,11 +501,13 @@ int optinclude_file(const char *name, int *argc, char **argv, char *x_argvblk,
|
||||
/* read line */
|
||||
linput(fp, line, 250);
|
||||
hts_lowcase(line);
|
||||
/* trim first: a blank line is skipped, not parsed as an option */
|
||||
hts_rtrim(line, HTS_REALSPACES);
|
||||
if (strnotempty(line)) {
|
||||
/* no comment line: # // ; */
|
||||
if (strchr("#/;", line[0]) == NULL) {
|
||||
/* right trim */
|
||||
a = line + strlen(line) - 1;
|
||||
while(is_realspace(*a))
|
||||
*(a--) = '\0';
|
||||
/* jump "set " and spaces */
|
||||
a = line;
|
||||
while(is_realspace(*a))
|
||||
@@ -575,6 +577,7 @@ int optinclude_file(const char *name, int *argc, char **argv, char *x_argvblk,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
fclose(fp);
|
||||
|
||||
102
src/htsback.c
102
src/htsback.c
@@ -596,6 +596,41 @@ 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,
|
||||
@@ -603,12 +638,10 @@ 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') {
|
||||
/* 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)) {
|
||||
if (!back_tmpname(back->tmpfile_buffer, sizeof(back->tmpfile_buffer),
|
||||
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;
|
||||
@@ -646,7 +679,11 @@ void back_refetch_backup(httrackp *opt, lien_back *const back) {
|
||||
if (fexist_utf8(back->tmpfile))
|
||||
hts_log_print(opt, LOG_WARNING, "replacing leftover backup %s",
|
||||
back->tmpfile);
|
||||
saved = hts_rename_over(back->url_sav, back->tmpfile);
|
||||
saved = hts_rename_over(opt, back->url_sav, back->tmpfile);
|
||||
/* 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);
|
||||
}
|
||||
if (!saved) {
|
||||
hts_log_print(opt, LOG_WARNING | LOG_ERRNO,
|
||||
@@ -670,16 +707,22 @@ static hts_boolean back_transfer_failed(const int statuscode) {
|
||||
}
|
||||
}
|
||||
|
||||
/* 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) {
|
||||
hts_boolean back_finalize_backup(httrackp *opt, lien_back *const back,
|
||||
hts_boolean commit) {
|
||||
const hts_boolean wanted = commit;
|
||||
|
||||
if (back->tmpfile == NULL || back->r.compressed)
|
||||
return;
|
||||
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;
|
||||
}
|
||||
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);
|
||||
@@ -687,12 +730,15 @@ static void back_finalize_backup(httrackp *opt, lien_back *const back,
|
||||
}
|
||||
/* On failure keep the backup: an orphaned temp beats losing the good copy.
|
||||
*/
|
||||
if (!hts_rename_over(back->tmpfile, back->url_sav))
|
||||
if (!hts_rename_over(opt, back->tmpfile, back->url_sav))
|
||||
hts_log_print(opt, LOG_WARNING | LOG_ERRNO,
|
||||
"could not restore %s; previous copy kept as %s",
|
||||
back->url_sav, back->tmpfile);
|
||||
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
|
||||
@@ -794,12 +840,21 @@ 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 + 4]; // room for ".u"
|
||||
char BIGSTK unpacked[HTS_URLMAXSIZE * 2];
|
||||
LLint size;
|
||||
|
||||
snprintf(unpacked, sizeof(unpacked), "%s.u", back[p].url_sav);
|
||||
if ((size = hts_codec_unpack(codec, back[p].tmpfile,
|
||||
unpacked)) >= 0) {
|
||||
/* 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) {
|
||||
back[p].r.size = back[p].r.totalsize = size;
|
||||
if (back[p].r.is_write) {
|
||||
/* Sample the previous copy now: the rename below replaces
|
||||
@@ -818,7 +873,7 @@ int back_finalize(httrackp * opt, cache_back * cache, struct_back * sback,
|
||||
"Read error when decompressing");
|
||||
}
|
||||
UNLINK(unpacked);
|
||||
} else if (hts_rename_over(unpacked, back[p].url_sav)) {
|
||||
} else if (hts_rename_over(opt, unpacked, back[p].url_sav)) {
|
||||
/* The temp bypassed filecreate(), which is what chmods. */
|
||||
#ifndef _WIN32
|
||||
chmod(back[p].url_sav, HTS_ACCESS_FILE);
|
||||
@@ -871,6 +926,7 @@ 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;
|
||||
}
|
||||
}
|
||||
@@ -883,7 +939,14 @@ 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. */
|
||||
back_finalize_backup(opt, &back[p], HTS_TRUE);
|
||||
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;
|
||||
}
|
||||
/* Write mode to disk */
|
||||
if (back[p].r.is_write && back[p].r.adr != NULL) {
|
||||
freet(back[p].r.adr);
|
||||
@@ -1719,6 +1782,7 @@ 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
|
||||
|
||||
@@ -143,6 +143,18 @@ 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);
|
||||
|
||||
@@ -491,7 +491,7 @@ char *bauth_prefix(char *prefix, const char *adr, const char *fil) {
|
||||
if (a)
|
||||
*a = '\0';
|
||||
if (strchr(prefix, '/')) {
|
||||
a = hts_lastcharptr(prefix);
|
||||
a = prefix + strlen(prefix) - 1;
|
||||
while(*a != '/')
|
||||
a--;
|
||||
*(a + 1) = '\0';
|
||||
|
||||
@@ -957,6 +957,35 @@ 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) {
|
||||
// ---
|
||||
@@ -985,6 +1014,7 @@ void cache_init(cache_back * cache, httrackp * opt) {
|
||||
StringBuff(opt->path_log),
|
||||
"hts-cache/new.zip")))) { // a previous cache exists.. rename it
|
||||
if (!hts_rename_over(
|
||||
opt,
|
||||
fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
|
||||
StringBuff(opt->path_log), "hts-cache/new.zip"),
|
||||
fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
|
||||
@@ -1025,8 +1055,9 @@ void cache_init(cache_back * cache, httrackp * opt) {
|
||||
// Corrupted ZIP file ? Try to repair!
|
||||
if (cache->zipInput == NULL && !cache->ro) {
|
||||
char *name;
|
||||
uLong repaired = 0;
|
||||
uLong repairedBytes = 0;
|
||||
const char *why;
|
||||
unsigned long repaired = 0;
|
||||
unsigned long repairedBytes = 0;
|
||||
|
||||
if (!cache->ro) {
|
||||
name =
|
||||
@@ -1039,25 +1070,16 @@ void cache_init(cache_back * cache, httrackp * opt) {
|
||||
}
|
||||
hts_log_print(opt, LOG_WARNING,
|
||||
"Cache: damaged cache, trying to repair");
|
||||
/* 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);
|
||||
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) {
|
||||
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: could not repair the cache");
|
||||
hts_log_print(opt, LOG_WARNING,
|
||||
"Cache: the repaired cache could not be reopened");
|
||||
}
|
||||
}
|
||||
// Opened ?
|
||||
|
||||
@@ -78,6 +78,14 @@ 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 */
|
||||
|
||||
@@ -3774,7 +3774,8 @@ int htsAddLink(htsmoduleStruct * str, char *link) {
|
||||
strcpybuff(codebase, heap(ptr)->fil);
|
||||
else
|
||||
strcpybuff(codebase, heap(heap(ptr)->precedent)->fil);
|
||||
a = hts_lastcharptr(codebase);
|
||||
// empty codebase has no last char; codebase-1 would underflow
|
||||
a = codebase[0] != '\0' ? codebase + strlen(codebase) - 1 : codebase;
|
||||
while((*a) && (*a != '/') && (a > codebase))
|
||||
a--;
|
||||
if (*a == '/')
|
||||
|
||||
@@ -2160,8 +2160,9 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
|
||||
case 'R':
|
||||
{
|
||||
char *name;
|
||||
uLong repaired = 0;
|
||||
uLong repairedBytes = 0;
|
||||
const char *why;
|
||||
unsigned long repaired = 0;
|
||||
unsigned long repairedBytes = 0;
|
||||
|
||||
if (fexist_utf8(fconcat(
|
||||
OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
|
||||
@@ -2184,24 +2185,15 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
|
||||
return 1;
|
||||
}
|
||||
fprintf(stderr, "Cache: trying to repair %s\n", name);
|
||||
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");
|
||||
why = cache_repair(opt, name, &repaired, &repairedBytes);
|
||||
if (why != NULL) {
|
||||
fprintf(stderr, "Cache: %s\n", why);
|
||||
return 1;
|
||||
}
|
||||
fprintf(
|
||||
stderr,
|
||||
"Cache: %d bytes successfully recovered in %d entries\n",
|
||||
(int) repairedBytes, (int) repaired);
|
||||
}
|
||||
return 0;
|
||||
break;
|
||||
|
||||
16
src/htsftp.c
16
src/htsftp.c
@@ -483,16 +483,19 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
|
||||
back->r.totalsize = size;
|
||||
}
|
||||
}
|
||||
// REST?
|
||||
if (fexist(back->url_sav) && (transfer_list == 0)) {
|
||||
/* Only over a copy back_add() judged partial: on --update every
|
||||
mirrored file exists, and resuming a complete one splices the
|
||||
old body into the new (#798). */
|
||||
if (back->range_req_size > 0 && (transfer_list == 0)) {
|
||||
strcpybuff(back->info, "rest");
|
||||
snprintf(line, sizeof(line), "REST " LLintP, (LLint) fsize(back->url_sav));
|
||||
snprintf(line, sizeof(line), "REST " LLintP,
|
||||
(LLint) back->range_req_size);
|
||||
send_line(soc_ctl, line);
|
||||
get_ftp_line(soc_ctl, line, sizeof(line), timeout);
|
||||
_CHECK_HALT_FTP;
|
||||
if ((line[0] == '3') || (line[0] == '2')) { // ok
|
||||
rest_understood = 1;
|
||||
} // sinon tant pis
|
||||
} // else never mind
|
||||
}
|
||||
} // sinon tant pis
|
||||
}
|
||||
@@ -617,9 +620,12 @@ int run_launch_ftp(FTPDownloadStruct * pStruct) {
|
||||
// Ok, connexion initiée
|
||||
//
|
||||
if (soc_dat != INVALID_SOCKET) {
|
||||
if (rest_understood) { // REST envoyée et comprise
|
||||
if (rest_understood) { // REST sent and understood
|
||||
file_notify(opt, back->url_adr, back->url_fil, back->url_sav, 0, 1,
|
||||
0);
|
||||
/* The bytes already on disk count too, or the completeness check
|
||||
below rejects every resumed transfer (#798). */
|
||||
back->r.size = back->range_req_size;
|
||||
back->r.fp = fileappend(&opt->state.strc, back->url_sav);
|
||||
} else {
|
||||
file_notify(opt, back->url_adr, back->url_fil, back->url_sav, 1, 1,
|
||||
|
||||
@@ -558,8 +558,7 @@ void help(const char *app, int more) {
|
||||
infomsg
|
||||
(" %q *include query string for local files (useless, for information purpose only) (%q0 don't include)");
|
||||
infomsg(" %g strip query keys for dedup ([host/pattern=]key1,key2,...)");
|
||||
infomsg
|
||||
(" o *generate output html file in case of error (404..) (o0 don't generate)");
|
||||
infomsg(" o *save the server's error pages (404..) (o0 discard them)");
|
||||
infomsg(" X *purge old files after update (X0 keep delete)");
|
||||
infomsg(" %p preserve html files 'as is' (identical to '-K4 -%F \"\"')");
|
||||
infomsg(" %T links conversion to UTF-8");
|
||||
|
||||
22
src/htslib.c
22
src/htslib.c
@@ -1322,7 +1322,7 @@ void treathead(t_cookie * cookie, const char *adr, const char *fil, htsblk * ret
|
||||
p++; // sauter espaces
|
||||
if ((int) strlen(rcvd + p) < 250) { // pas trop long?
|
||||
char tmp[256];
|
||||
char *a = NULL;
|
||||
char *a = NULL, *b = NULL;
|
||||
|
||||
strcpybuff(tmp, rcvd + p);
|
||||
a = strstr(tmp, "filename=");
|
||||
@@ -1335,9 +1335,15 @@ void treathead(t_cookie * cookie, const char *adr, const char *fil, htsblk * ret
|
||||
|
||||
while((c = strchr(a, '/'))) /* skip all / (see RFC2616) */
|
||||
a = c + 1;
|
||||
hts_rtrim(a, HTS_SPACES);
|
||||
if ((int) strlen(a) < 200) { // pas trop long?
|
||||
strcpybuff(retour->cdispo, a);
|
||||
b = a + strlen(a) - 1;
|
||||
while(is_space(*b))
|
||||
b--;
|
||||
b++;
|
||||
if (b) {
|
||||
*b = '\0';
|
||||
if ((int) strlen(a) < 200) { // pas trop long?
|
||||
strcpybuff(retour->cdispo, a);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3309,9 +3315,8 @@ int ishtml(httrackp * opt, const char *fil) {
|
||||
}
|
||||
|
||||
/* Search for known ext */
|
||||
for (a = hts_lastcharptr(fil_noquery);
|
||||
*a != '.' && *a != '/' && a > fil_noquery; a--)
|
||||
;
|
||||
for(a = fil_noquery + strlen(fil_noquery) - 1;
|
||||
*a != '.' && *a != '/' && a > fil_noquery; a--) ;
|
||||
if (*a == '.') { // a une extension
|
||||
char BIGSTK fil_noquery[HTS_URLMAXSIZE * 2];
|
||||
char *b;
|
||||
@@ -4248,8 +4253,9 @@ HTSEXT_API hts_boolean get_httptype_sized(httrackp *opt, char *s, size_t ssize,
|
||||
return 1;
|
||||
} else {
|
||||
/* Check html -> text/html */
|
||||
const char *a = hts_lastcharptr(fil);
|
||||
const char *a = fil + strlen(fil) - 1;
|
||||
|
||||
/* a < fil when fil is empty: bound before dereferencing */
|
||||
while ((a > fil) && (*a != '.') && (*a != '/'))
|
||||
a--;
|
||||
if (a >= fil && *a == '.' && strlen(a) < 32) {
|
||||
|
||||
140
src/htsname.c
140
src/htsname.c
@@ -74,6 +74,41 @@ 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;
|
||||
@@ -823,7 +858,7 @@ int url_savename(lien_adrfilsave *const afs,
|
||||
// Change the extension? e.g. php3 saved as html, cgi as html or gif/xbm
|
||||
// depending on the resolved type.
|
||||
if (ext_chg && !opt->no_type_change) {
|
||||
char *a = hts_lastcharptr(fil);
|
||||
char *a = fil + strlen(fil) - 1;
|
||||
|
||||
if ((opt->debug > 1) && (opt->log != NULL)) {
|
||||
if (ext_chg == 1)
|
||||
@@ -858,7 +893,7 @@ int url_savename(lien_adrfilsave *const afs,
|
||||
}
|
||||
// Rechercher premier / et dernier .
|
||||
{
|
||||
const char *a = hts_lastcharptr(fil);
|
||||
const char *a = fil + strlen(fil) - 1;
|
||||
|
||||
// passer structures
|
||||
start_pos = fil;
|
||||
@@ -1203,29 +1238,29 @@ int url_savename(lien_adrfilsave *const afs,
|
||||
switch (opt->savename_type % 100) {
|
||||
case 4:
|
||||
case 5:{ // séparer par types
|
||||
const char *a = hts_lastcharptr(fil);
|
||||
const char *a = fil + strlen(fil) - 1;
|
||||
|
||||
// passer structures
|
||||
while ((a > fil) && (*a != '/') && (*a != '\\'))
|
||||
a--;
|
||||
if ((*a == '/') || (*a == '\\'))
|
||||
a++;
|
||||
|
||||
// html?
|
||||
if ((ext_chg != 0) ? (ishtml_ext(ext) == 1) : (ishtml(opt, fil) == 1)) {
|
||||
if (opt->savename_type % 100 == 5)
|
||||
strcatbuff(afs->save, "html/");
|
||||
} else {
|
||||
const char *a = hts_lastcharptr(fil);
|
||||
|
||||
while ((a > fil) && (*a != '/') && (*a != '.'))
|
||||
// passer structures
|
||||
while((a > fil) && (*a != '/') && (*a != '\\'))
|
||||
a--;
|
||||
if (*a != '.')
|
||||
strcatbuff(afs->save, "other");
|
||||
else
|
||||
strcatbuff(afs->save, a + 1);
|
||||
strcatbuff(afs->save, "/");
|
||||
}
|
||||
if ((*a == '/') || (*a == '\\'))
|
||||
a++;
|
||||
|
||||
// html?
|
||||
if ((ext_chg != 0) ? (ishtml_ext(ext) == 1) : (ishtml(opt, fil) == 1)) {
|
||||
if (opt->savename_type % 100 == 5)
|
||||
strcatbuff(afs->save, "html/");
|
||||
} else {
|
||||
const char *a = fil + strlen(fil) - 1;
|
||||
|
||||
while((a > fil) && (*a != '/') && (*a != '.'))
|
||||
a--;
|
||||
if (*a != '.')
|
||||
strcatbuff(afs->save, "other");
|
||||
else
|
||||
strcatbuff(afs->save, a + 1);
|
||||
strcatbuff(afs->save, "/");
|
||||
}
|
||||
/*strcatbuff(save,a); */
|
||||
/* add name */
|
||||
ADD_STANDARD_NAME(0);
|
||||
@@ -1258,7 +1293,7 @@ int url_savename(lien_adrfilsave *const afs,
|
||||
}
|
||||
afs->save[i + j] = '\0';
|
||||
// ajouter extension
|
||||
a = hts_lastcharptr(fil);
|
||||
a = fil + strlen(fil) - 1;
|
||||
while((a > fil) && (*a != '/') && (*a != '.'))
|
||||
a--;
|
||||
if (*a == '.') {
|
||||
@@ -1303,7 +1338,7 @@ int url_savename(lien_adrfilsave *const afs,
|
||||
// cela évite les /chez/toto et les /chez/toto/index.html incompatibles
|
||||
if (opt->savename_type != -1 &&
|
||||
opt->savename_delayed != HTS_SAVENAME_DELAYED_HARD) {
|
||||
char *a = hts_lastcharptr(afs->save);
|
||||
char *a = afs->save + strlen(afs->save) - 1;
|
||||
|
||||
while((a > afs->save) && (*a != '.') && (*a != '/'))
|
||||
a--;
|
||||
@@ -1369,35 +1404,12 @@ 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
|
||||
/* 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++;
|
||||
}
|
||||
}
|
||||
escapeReservedNames(afs->save, sizeof(afs->save), hts_tbdev);
|
||||
|
||||
/* Strip ending . or ' ' forbidden on windoz */
|
||||
cleanEndingSpaceOrDot(afs->save);
|
||||
@@ -1420,10 +1432,8 @@ int url_savename(lien_adrfilsave *const afs,
|
||||
if (opt->savename_83 > 0) {
|
||||
char *a, *last;
|
||||
|
||||
for (last = hts_lastcharptr(afs->save);
|
||||
last != afs->save && *last != '/' && *last != '\\' && *last != '.';
|
||||
last--)
|
||||
;
|
||||
for(last = afs->save + strlen(afs->save) - 1;
|
||||
last != afs->save && *last != '/' && *last != '\\' && *last != '.'; last--) ;
|
||||
if (*last != '.') {
|
||||
last = NULL;
|
||||
}
|
||||
@@ -1678,8 +1688,8 @@ int url_savename(lien_adrfilsave *const afs,
|
||||
#endif
|
||||
} else { // utilisé par un AUTRE, changer de nom
|
||||
char BIGSTK tempo[HTS_URLMAXSIZE * 2];
|
||||
char *a = hts_lastcharptr(afs->save);
|
||||
size_t stem;
|
||||
char *a = afs->save + strlen(afs->save) - 1;
|
||||
char *b;
|
||||
int n = 2;
|
||||
char collisionSeparator =
|
||||
((opt->savename_83 != HTS_SAVENAME_83_ISO9660) ? '-' : '_');
|
||||
@@ -1701,16 +1711,18 @@ int url_savename(lien_adrfilsave *const afs,
|
||||
strcatbuff(tempo, afs->save);
|
||||
|
||||
// tester la présence d'un -xx (ex: index-2.html -> index-3.html)
|
||||
stem = hts_rtrimlen(tempo, "0123456789");
|
||||
if (stem != 0 && tempo[stem - 1] == collisionSeparator) {
|
||||
sscanf(tempo + stem, "%d", &n);
|
||||
tempo[stem - 1] = '\0'; // couper
|
||||
b = tempo + strlen(tempo) - 1;
|
||||
while(isdigit((unsigned char) *b))
|
||||
b--;
|
||||
if (*b == collisionSeparator) {
|
||||
sscanf(b + 1, "%d", &n);
|
||||
*b = '\0'; // couper
|
||||
n++; // plus un
|
||||
}
|
||||
// en plus il faut gérer le 8-3 .. pas facile le client
|
||||
if (opt->savename_83) {
|
||||
int max;
|
||||
char *a = hts_lastcharptr(tempo);
|
||||
char *a = tempo + strlen(tempo) - 1;
|
||||
|
||||
while((a > tempo) && (*a != '/'))
|
||||
a--;
|
||||
|
||||
@@ -2057,7 +2057,7 @@ int htsparse(htsmoduleStruct * str, htsmoduleStructExtended * stre) {
|
||||
}
|
||||
q = strchr(a, '?'); // ne pas traiter après '?'
|
||||
if (!q)
|
||||
q = hts_lastcharptr(a);
|
||||
q = a + strlen(a) - 1;
|
||||
while((p = strstr(a, "//")) && (!done)) { // remplacer // par /
|
||||
if (p > q) { // après le ? (toto.cgi?param=1//2.3)
|
||||
done = 1; // stopper
|
||||
@@ -2208,7 +2208,7 @@ int htsparse(htsmoduleStruct * str, htsmoduleStructExtended * stre) {
|
||||
// OUI!!
|
||||
#if HTS_TILDE_SLASH
|
||||
if (hts_lastchar(lien) != '/') {
|
||||
char *a = hts_lastcharptr(lien);
|
||||
char *a = lien + strlen(lien) - 1;
|
||||
|
||||
// éviter aussi index~1.html
|
||||
while(a > lien && (*a != '~') && (*a != '/')
|
||||
@@ -2254,7 +2254,7 @@ int htsparse(htsmoduleStruct * str, htsmoduleStructExtended * stre) {
|
||||
// vérifier que l'on ne doit pas ajouter de .class
|
||||
if (!error) {
|
||||
if (add_class) {
|
||||
char *a = hts_lastcharptr(lien);
|
||||
char *a = lien + strlen(lien) - 1;
|
||||
|
||||
while((a > lien) && (*a != '/') && (*a != '.'))
|
||||
a--;
|
||||
@@ -2309,7 +2309,7 @@ int htsparse(htsmoduleStruct * str, htsmoduleStructExtended * stre) {
|
||||
{
|
||||
char *a;
|
||||
|
||||
a = hts_lastcharptr(lien);
|
||||
a = lien + strlen(lien) - 1;
|
||||
while((*a) && (*a != '/') && (a > lien))
|
||||
a--;
|
||||
if (*a == '/') {
|
||||
@@ -2320,8 +2320,8 @@ int htsparse(htsmoduleStruct * str, htsmoduleStructExtended * stre) {
|
||||
}
|
||||
|
||||
if (!error) { // pas d'erreur?
|
||||
if (p_type == 2) { // code ET PAS codebase
|
||||
char *a = hts_lastcharptr(lien);
|
||||
if (p_type == 2) { // code ET PAS codebase
|
||||
char *a = lien + strlen(lien) - 1;
|
||||
char *start_of_filename = jump_identification(lien);
|
||||
|
||||
if (start_of_filename != NULL
|
||||
|
||||
@@ -569,38 +569,6 @@ static HTS_INLINE HTS_UNUSED hts_boolean hts_choplastchar(char *s) {
|
||||
return HTS_FALSE;
|
||||
}
|
||||
|
||||
/* htslib.h's is_space() and is_realspace() as hts_rtrim() sets; -#test=rtrim
|
||||
keeps them in sync. */
|
||||
#define HTS_SPACES " \"\n\r\t\f\v'"
|
||||
#define HTS_REALSPACES " \n\r\t\f\v"
|
||||
|
||||
/* Length of s once its trailing bytes from set are dropped; 0 if they all are.
|
||||
Counts down from the end, so it stops at s rather than below the buffer. */
|
||||
static HTS_INLINE HTS_UNUSED size_t hts_rtrimlen(const char *s,
|
||||
const char *set) {
|
||||
size_t len = strlen(s);
|
||||
|
||||
while (len != 0 && strchr(set, s[len - 1]) != NULL)
|
||||
len--;
|
||||
return len;
|
||||
}
|
||||
|
||||
/* Drop the trailing bytes of s that occur in set. */
|
||||
static HTS_INLINE HTS_UNUSED void hts_rtrim(char *s, const char *set) {
|
||||
s[hts_rtrimlen(s, set)] = '\0';
|
||||
}
|
||||
|
||||
/* Offset of the last character of s, or 0 when s is empty. */
|
||||
static HTS_INLINE HTS_UNUSED size_t hts_lastcharoffset(const char *s) {
|
||||
const size_t len = strlen(s);
|
||||
|
||||
return len != 0 ? len - 1 : 0;
|
||||
}
|
||||
|
||||
/* Address of the last character of S, or of its terminating NUL when S is
|
||||
empty. S is evaluated twice, so pass an lvalue. */
|
||||
#define hts_lastcharptr(S) ((S) + hts_lastcharoffset(S))
|
||||
|
||||
/* Thin aliases over the libc allocator/memcpy (historical "t" suffix); no
|
||||
added bounds checking. freet() also NULLs the freed pointer and tolerates
|
||||
NULL. memcpybuff() despite the name is a raw memcpy: the caller owns the
|
||||
|
||||
@@ -6137,10 +6137,9 @@ static hts_boolean ro_is(const char *path, const char *data) {
|
||||
}
|
||||
|
||||
// -#test=renameover <dir>: hts_rename_over() must replace an existing dst and
|
||||
// never delete one it did not replace (#779). Which half is live depends on
|
||||
// never lose one it did not replace (#779, #790). Which half is live depends on
|
||||
// what rename() does to an existing target, so probe that and name the regime.
|
||||
static int st_renameover(httrackp *opt, int argc, char **argv) {
|
||||
(void) opt;
|
||||
if (argc < 1) {
|
||||
fprintf(stderr, "renameover: needs a writable base dir\n");
|
||||
return 1;
|
||||
@@ -6169,7 +6168,7 @@ static int st_renameover(httrackp *opt, int argc, char **argv) {
|
||||
ro_put(dst, "old");
|
||||
if (replaceable) {
|
||||
/* An existing dst must still be replaced: the unlink is for this. */
|
||||
if (!hts_rename_over(src, dst)) {
|
||||
if (!hts_rename_over(opt, src, dst)) {
|
||||
fprintf(stderr, "renameover: replacing an existing dst failed: %s\n",
|
||||
strerror(errno));
|
||||
err++;
|
||||
@@ -6179,7 +6178,7 @@ static int st_renameover(httrackp *opt, int argc, char **argv) {
|
||||
}
|
||||
} else {
|
||||
/* A failure the unlink cannot fix must leave dst as it was. */
|
||||
if (hts_rename_over(src, dst)) {
|
||||
if (hts_rename_over(opt, src, dst)) {
|
||||
fprintf(stderr, "renameover: an unfixable failure reported success\n");
|
||||
err++;
|
||||
}
|
||||
@@ -6189,10 +6188,36 @@ static int st_renameover(httrackp *opt, int argc, char **argv) {
|
||||
}
|
||||
}
|
||||
|
||||
/* A directory in the way is not something the caller asked to replace: it
|
||||
must be refused, never parked aside and orphaned. */
|
||||
(void) UNLINK(dst);
|
||||
ro_put(src, "new");
|
||||
if (MKDIR(dst) == 0) {
|
||||
char parked[sizeof(dst) + 16];
|
||||
|
||||
snprintf(parked, sizeof(parked), "%s.hts-old0", dst);
|
||||
if (hts_rename_over(opt, src, dst)) {
|
||||
fprintf(stderr, "renameover: a directory at dst reported success\n");
|
||||
err++;
|
||||
}
|
||||
if (!ro_is(src, "new")) {
|
||||
fprintf(stderr, "renameover: a directory at dst consumed src\n");
|
||||
err++;
|
||||
}
|
||||
/* RMDIR only succeeds on a directory that is there, so it doubles as the
|
||||
probe: the parked name must not exist at all. */
|
||||
if (RMDIR(parked) == 0 || fexist_utf8(parked)) {
|
||||
fprintf(stderr, "renameover: a directory at dst was parked aside\n");
|
||||
err++;
|
||||
}
|
||||
(void) RMDIR(dst);
|
||||
}
|
||||
(void) UNLINK(src);
|
||||
|
||||
/* A missing src must leave dst alone and report failure. */
|
||||
(void) UNLINK(src);
|
||||
ro_put(dst, "keep");
|
||||
if (hts_rename_over(src, dst)) {
|
||||
if (hts_rename_over(opt, src, dst)) {
|
||||
fprintf(stderr, "renameover: a missing src reported success\n");
|
||||
err++;
|
||||
}
|
||||
@@ -6203,16 +6228,177 @@ static int st_renameover(httrackp *opt, int argc, char **argv) {
|
||||
|
||||
/* Same, with dst absent too: nothing to lose, still a failure. */
|
||||
(void) UNLINK(dst);
|
||||
if (hts_rename_over(src, dst)) {
|
||||
if (hts_rename_over(opt, src, dst)) {
|
||||
fprintf(stderr, "renameover: a missing src and dst reported success\n");
|
||||
err++;
|
||||
}
|
||||
|
||||
/* The aside fallback, driven directly: a clobbering rename() never reaches
|
||||
it. Skipped in the refused regime, where no rename at all succeeds. */
|
||||
if (replaceable) {
|
||||
char aside[sizeof(dst) + 16], keep[sizeof(dst) + 16];
|
||||
|
||||
snprintf(aside, sizeof(aside), "%s.hts-old0", dst);
|
||||
snprintf(keep, sizeof(keep), "%s.hts-old1", dst);
|
||||
(void) UNLINK(aside);
|
||||
(void) UNLINK(keep);
|
||||
ro_put(src, "new");
|
||||
ro_put(dst, "old");
|
||||
if (!hts_rename_over_aside_selftest(opt, src, dst)) {
|
||||
fprintf(stderr, "renameover: the aside fallback failed: %s\n",
|
||||
strerror(errno));
|
||||
err++;
|
||||
} else if (!ro_is(dst, "new") || fexist_utf8(src) || fexist_utf8(aside)) {
|
||||
fprintf(stderr, "renameover: the aside fallback did not replace dst\n");
|
||||
err++;
|
||||
}
|
||||
|
||||
/* #790: the retry fails (no src). The old content must survive, back at dst
|
||||
or, when the move back fails too, under the parked name it is logged as.
|
||||
Name the outcome so a leg cannot pass having tested the other one. */
|
||||
(void) UNLINK(src);
|
||||
ro_put(dst, "old");
|
||||
if (hts_rename_over_aside_selftest(opt, src, dst)) {
|
||||
fprintf(stderr, "renameover: a failed aside retry reported success\n");
|
||||
err++;
|
||||
}
|
||||
if (ro_is(dst, "old") && !fexist_utf8(aside)) {
|
||||
printf("renameover: restore back\n");
|
||||
} else if (ro_is(aside, "old") && !fexist_utf8(dst)) {
|
||||
printf("renameover: restore parked\n");
|
||||
(void) UNLINK(aside);
|
||||
ro_put(dst, "old");
|
||||
} else {
|
||||
fprintf(stderr, "renameover: a failed aside retry lost the old copy\n");
|
||||
err++;
|
||||
}
|
||||
|
||||
/* An unrelated file already sitting on the aside name must survive. */
|
||||
ro_put(src, "new");
|
||||
ro_put(aside, "mine");
|
||||
if (!hts_rename_over_aside_selftest(opt, src, dst)) {
|
||||
fprintf(stderr, "renameover: a taken aside name failed the move: %s\n",
|
||||
strerror(errno));
|
||||
err++;
|
||||
} else if (!ro_is(dst, "new") || !ro_is(aside, "mine") ||
|
||||
fexist_utf8(keep)) {
|
||||
fprintf(stderr, "renameover: a taken aside name was not skipped\n");
|
||||
err++;
|
||||
}
|
||||
(void) UNLINK(aside);
|
||||
(void) UNLINK(keep);
|
||||
|
||||
/* A directory there reads as free to the probe, so the park must skip it
|
||||
on the refusal rather than give up. */
|
||||
ro_put(src, "new");
|
||||
ro_put(dst, "old");
|
||||
if (MKDIR(aside) == 0) {
|
||||
if (!hts_rename_over_aside_selftest(opt, src, dst)) {
|
||||
fprintf(stderr,
|
||||
"renameover: a directory on the aside name blocked the "
|
||||
"move: %s\n",
|
||||
strerror(errno));
|
||||
err++;
|
||||
} else if (!ro_is(dst, "new") || fexist_utf8(keep)) {
|
||||
fprintf(stderr, "renameover: a directory on the aside name was not "
|
||||
"skipped\n");
|
||||
err++;
|
||||
}
|
||||
(void) RMDIR(aside);
|
||||
}
|
||||
(void) UNLINK(keep);
|
||||
}
|
||||
|
||||
(void) UNLINK(src);
|
||||
(void) UNLINK(dst);
|
||||
printf("renameover: %s\n", err ? "FAIL" : "OK");
|
||||
return err;
|
||||
}
|
||||
|
||||
// -#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) {
|
||||
@@ -6710,10 +6896,9 @@ static int st_changes_race(httrackp *opt, int argc, char **argv) {
|
||||
return err;
|
||||
}
|
||||
|
||||
/* The x[strlen(x) - 1] class (#770) and its pointer spelling x + strlen(x) - 1
|
||||
(#781). The string starts mid-arena so the byte it must not touch is a real
|
||||
neighbour; poisoned with '#', not 0, or a stray NUL terminator would read as
|
||||
untouched. */
|
||||
/* The x[strlen(x) - 1] class (#770). The string starts mid-arena so the byte
|
||||
it must not touch is a real neighbour; poisoned with '#', not 0, or a stray
|
||||
NUL terminator would read as untouched. */
|
||||
static int st_lastchar(httrackp *opt, int argc, char **argv) {
|
||||
enum { off = 8 };
|
||||
|
||||
@@ -6780,52 +6965,11 @@ static int st_lastchar(httrackp *opt, int argc, char **argv) {
|
||||
CHECK(s[0] == '\0');
|
||||
CHECK(arena[guard] == '#');
|
||||
|
||||
/* the pointer spelling (#781): on an empty string the address must be the
|
||||
terminating NUL, never the byte before it */
|
||||
REPOISON("");
|
||||
CHECK(hts_lastcharoffset(s) == 0);
|
||||
CHECK(hts_lastcharptr(s) == s);
|
||||
CHECK(*hts_lastcharptr(s) == '\0');
|
||||
*hts_lastcharptr(s) = 'Z'; /* a write through it must stay inside s */
|
||||
CHECK(arena[guard] == '#');
|
||||
CHECK(s[0] == 'Z');
|
||||
|
||||
/* the neighbour must not be mistaken for the string's own last byte */
|
||||
REPOISON("");
|
||||
arena[guard] = '/';
|
||||
CHECK(hts_lastcharptr(s) == s);
|
||||
CHECK(*hts_lastcharptr(s) != '/');
|
||||
CHECK(arena[guard] == '/');
|
||||
|
||||
/* the walk-back loops the sites use must stop at once on an empty string */
|
||||
REPOISON("");
|
||||
{
|
||||
const char *p = hts_lastcharptr(s);
|
||||
int steps = 0;
|
||||
|
||||
while (p > s && *p != '/')
|
||||
p--, steps++;
|
||||
CHECK(steps == 0);
|
||||
CHECK(p == s);
|
||||
}
|
||||
|
||||
REPOISON("ab/");
|
||||
CHECK(hts_lastcharoffset(s) == 2);
|
||||
CHECK(hts_lastcharptr(s) == s + 2);
|
||||
CHECK(*hts_lastcharptr(s) == '/');
|
||||
REPOISON("/");
|
||||
CHECK(hts_lastcharptr(s) == s);
|
||||
CHECK(*hts_lastcharptr(s) == '/');
|
||||
CHECK(arena[guard] == '#');
|
||||
|
||||
/* control: the canary must be able to fail, or the checks above prove
|
||||
nothing. Clobber it exactly as the unguarded idiom would. */
|
||||
REPOISON("");
|
||||
s[-1] = '\0';
|
||||
CHECK(arena[guard] != '#');
|
||||
REPOISON("");
|
||||
*(s + strlen(s) - 1) = 'X';
|
||||
CHECK(arena[guard] != '#');
|
||||
|
||||
#undef REPOISON
|
||||
#undef CHECK
|
||||
@@ -6834,91 +6978,6 @@ static int st_lastchar(httrackp *opt, int argc, char **argv) {
|
||||
return err;
|
||||
}
|
||||
|
||||
/* hts_rtrim() and the sets it is called with. The string starts mid-arena, and
|
||||
the byte below it is poisoned with '#' rather than 0, or the stray NUL the
|
||||
old loop wrote there would read as untouched. */
|
||||
static int st_rtrim(httrackp *opt, int argc, char **argv) {
|
||||
enum { off = 8 };
|
||||
|
||||
char arena[24];
|
||||
char *const s = &arena[off];
|
||||
const int guard = off - 1;
|
||||
int err = 0;
|
||||
int c;
|
||||
|
||||
(void) opt;
|
||||
(void) argc;
|
||||
(void) argv;
|
||||
|
||||
#define REPOISON(str) \
|
||||
do { \
|
||||
memset(arena, '#', sizeof(arena)); \
|
||||
strlcpybuff(s, (str), sizeof(arena) - off); \
|
||||
} while (0)
|
||||
#define CHECK(cond) \
|
||||
do { \
|
||||
if (!(cond)) { \
|
||||
printf(" FAIL line %d: %s\n", __LINE__, #cond); \
|
||||
err = 1; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
/* nothing but spaces: the case that ran the old loop off the front */
|
||||
REPOISON(" ");
|
||||
hts_rtrim(s, HTS_REALSPACES);
|
||||
CHECK(s[0] == '\0');
|
||||
CHECK(arena[guard] == '#');
|
||||
|
||||
/* a space sitting below the string must not be eaten as if it were part of
|
||||
it, which is exactly what the old loop did */
|
||||
REPOISON(" ");
|
||||
arena[guard] = ' ';
|
||||
hts_rtrim(s, HTS_REALSPACES);
|
||||
CHECK(s[0] == '\0');
|
||||
CHECK(arena[guard] == ' ');
|
||||
|
||||
REPOISON("");
|
||||
hts_rtrim(s, HTS_REALSPACES);
|
||||
CHECK(s[0] == '\0');
|
||||
CHECK(arena[guard] == '#');
|
||||
|
||||
REPOISON("a b \t\r\n");
|
||||
hts_rtrim(s, HTS_REALSPACES);
|
||||
CHECK(strcmp(s, "a b") == 0);
|
||||
CHECK(arena[guard] == '#');
|
||||
|
||||
REPOISON("ab");
|
||||
hts_rtrim(s, HTS_REALSPACES);
|
||||
CHECK(strcmp(s, "ab") == 0);
|
||||
|
||||
/* quotes count as space for is_space() but not for is_realspace() */
|
||||
REPOISON("v\" ");
|
||||
hts_rtrim(s, HTS_REALSPACES);
|
||||
CHECK(strcmp(s, "v\"") == 0);
|
||||
REPOISON("v\" ");
|
||||
hts_rtrim(s, HTS_SPACES);
|
||||
CHECK(strcmp(s, "v") == 0);
|
||||
|
||||
/* the sets must stay the macros they stand for */
|
||||
for (c = 1; c < 256; c++) {
|
||||
const char b = (char) c;
|
||||
|
||||
CHECK((strchr(HTS_SPACES, b) != NULL) == (is_space(b) != 0));
|
||||
CHECK((strchr(HTS_REALSPACES, b) != NULL) == (is_realspace(b) != 0));
|
||||
}
|
||||
|
||||
/* control: the canary must be able to fail */
|
||||
REPOISON(" ");
|
||||
s[-1] = '\0';
|
||||
CHECK(arena[guard] != '#');
|
||||
|
||||
#undef REPOISON
|
||||
#undef CHECK
|
||||
|
||||
printf("rtrim self-test: %s\n", err ? "FAIL" : "OK");
|
||||
return err;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/* Registry: name -> handler, with a usage hint and a one-line description. */
|
||||
/* ------------------------------------------------------------ */
|
||||
@@ -6974,10 +7033,8 @@ static const struct selftest_entry {
|
||||
{"strsafe", "[overflow|overflow-buff|overflow-src [str]]",
|
||||
"bounded string-op self-test", st_strsafe},
|
||||
{"copyopt", "", "copy_htsopt option-copy self-test", st_copyopt},
|
||||
{"lastchar", "",
|
||||
"last-char helpers never index before the buffer (#770, #781)",
|
||||
{"lastchar", "", "last-char helpers never index before the buffer (#770)",
|
||||
st_lastchar},
|
||||
{"rtrim", "", "hts_rtrim never walks below the buffer", st_rtrim},
|
||||
{"changes", "", "--changes bucket accounting and JSON escaping (#714)",
|
||||
st_changes},
|
||||
{"changes-race", "<dir>", "--changes under a late transfer thread (#714)",
|
||||
@@ -7092,6 +7149,9 @@ 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},
|
||||
|
||||
@@ -1109,7 +1109,7 @@ hts_boolean singlefile_rewrite_file(httrackp *opt, const char *root,
|
||||
HTS_ACCESS_FILE);
|
||||
#endif
|
||||
if (ok)
|
||||
ok = hts_rename_over(StringBuff(tmp), page_path);
|
||||
ok = hts_rename_over(opt, StringBuff(tmp), page_path);
|
||||
if (!ok) {
|
||||
hts_log_print(opt, LOG_ERROR, "single-file: could not rewrite %s",
|
||||
page_path);
|
||||
|
||||
@@ -324,7 +324,8 @@ int lienrelatif(char *s, size_t ssize, const char *link, const char *curr_fil) {
|
||||
curr = _curr;
|
||||
strlcpybuff(curr, curr_fil, sizeof(_curr));
|
||||
if ((a = strchr(curr, '?')) == NULL) { // cut at the ? (query parameters)
|
||||
a = hts_lastcharptr(curr);
|
||||
// an empty path has no last character: curr-1 would read before the buffer
|
||||
a = curr[0] != '\0' ? curr + strlen(curr) - 1 : curr;
|
||||
}
|
||||
while((*a != '/') && (a > curr))
|
||||
a--; // chercher dernier / du chemin courant
|
||||
@@ -1437,21 +1438,75 @@ HTSEXT_API hts_boolean hts_findissystem(find_handle find) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
hts_boolean hts_rename_over(const char *src, const char *dst) {
|
||||
/* Park cdst under a free sibling name; caside receives it. */
|
||||
static hts_boolean rename_park_aside(char *caside, size_t size,
|
||||
const char *cdst) {
|
||||
int i;
|
||||
|
||||
for (i = 0; i < 16; i++) {
|
||||
if (!slprintfbuff(caside, size, "%s.hts-old%d", cdst, i))
|
||||
return HTS_FALSE;
|
||||
/* Skip a name the mirror already holds: POSIX rename() would clobber it
|
||||
(#774). A non-regular entry reads as free and the rename refuses it. */
|
||||
if (fexist_utf8(caside))
|
||||
continue;
|
||||
if (RENAME(cdst, caside) == 0)
|
||||
return HTS_TRUE;
|
||||
}
|
||||
return HTS_FALSE;
|
||||
}
|
||||
|
||||
/* cdst is in the way of the move: park it, retry, and put it back if the retry
|
||||
fails too. Unlinking it instead would leave nothing at all (#790). */
|
||||
static hts_boolean rename_over_aside(httrackp *opt, const char *csrc,
|
||||
const char *cdst) {
|
||||
char caside[CATBUFF_SIZE];
|
||||
int err;
|
||||
|
||||
/* Only a regular file may be parked: a directory in the way is not what the
|
||||
caller asked to replace, and parking it orphans it (UNLINK cannot drop). */
|
||||
if (!fexist_utf8(cdst))
|
||||
return HTS_FALSE;
|
||||
if (!rename_park_aside(caside, sizeof(caside), cdst))
|
||||
return HTS_FALSE;
|
||||
if (RENAME(csrc, cdst) == 0) {
|
||||
(void) UNLINK(caside);
|
||||
return HTS_TRUE;
|
||||
}
|
||||
err = errno;
|
||||
/* Retry once, then name the parked copy: nothing else on disk or in the log
|
||||
points at it, and an --update purge would delete it unnoticed. */
|
||||
if (RENAME(caside, cdst) != 0 && RENAME(caside, cdst) != 0)
|
||||
hts_log_print(opt, LOG_WARNING | LOG_ERRNO,
|
||||
"could not put %s back; its previous content is now %s", cdst,
|
||||
caside);
|
||||
errno = err;
|
||||
return HTS_FALSE;
|
||||
}
|
||||
|
||||
hts_boolean hts_rename_over(httrackp *opt, const char *src, const char *dst) {
|
||||
char csrc[CATBUFF_SIZE], cdst[CATBUFF_SIZE];
|
||||
|
||||
fconv(csrc, sizeof(csrc), src);
|
||||
fconv(cdst, sizeof(cdst), dst);
|
||||
if (RENAME(csrc, cdst) == 0)
|
||||
return HTS_TRUE;
|
||||
/* Only a dst in the way is something the unlink can clear, and the CRT maps
|
||||
that to EEXIST; it keeps EACCES for a src another process holds, where
|
||||
removing dst would lose a file the retry cannot replace (#790). The src
|
||||
check covers a CRT that reports neither. */
|
||||
/* Only a dst in the way is something the fallback can clear, and the CRT maps
|
||||
that to EEXIST; it keeps EACCES for a src another process holds, where the
|
||||
retry would fail the same way. The src check covers a CRT that reports
|
||||
neither. */
|
||||
const int err = errno;
|
||||
|
||||
if (err != EEXIST || !fexist_utf8(src))
|
||||
return HTS_FALSE;
|
||||
(void) UNLINK(cdst);
|
||||
return RENAME(csrc, cdst) == 0 ? HTS_TRUE : HTS_FALSE;
|
||||
return rename_over_aside(opt, csrc, cdst);
|
||||
}
|
||||
|
||||
hts_boolean hts_rename_over_aside_selftest(httrackp *opt, const char *src,
|
||||
const char *dst) {
|
||||
char csrc[CATBUFF_SIZE], cdst[CATBUFF_SIZE];
|
||||
|
||||
fconv(csrc, sizeof(csrc), src);
|
||||
fconv(cdst, sizeof(cdst), dst);
|
||||
return rename_over_aside(opt, csrc, cdst);
|
||||
}
|
||||
|
||||
@@ -137,11 +137,17 @@ HTSEXT_API hts_boolean hts_findisdir(find_handle find);
|
||||
HTSEXT_API hts_boolean hts_findisfile(find_handle find);
|
||||
HTSEXT_API hts_boolean hts_findissystem(find_handle find);
|
||||
|
||||
/* Move src onto dst, replacing an existing dst; HTS_TRUE on success. Both
|
||||
paths are fconv()'d. dst is removed only to make room for a src that exists,
|
||||
so a caller whose src was never written keeps its dst; a retry that still
|
||||
fails does not (#790). */
|
||||
hts_boolean hts_rename_over(const char *src, const char *dst);
|
||||
/* Move src onto dst, replacing an existing dst; HTS_TRUE on success. Both paths
|
||||
are fconv()'d. A dst in the way is parked under a sibling name rather than
|
||||
removed, so the old content survives a failure: back at dst, or under that
|
||||
sibling (named in the log) when the move back failed too. Not atomic: a crash
|
||||
between the two renames leaves dst absent and its content beside it. */
|
||||
hts_boolean hts_rename_over(httrackp *opt, const char *src, const char *dst);
|
||||
|
||||
/* Selftest hook: run the aside fallback directly, on a platform whose rename()
|
||||
never reaches it. Both paths are fconv()'d. */
|
||||
hts_boolean hts_rename_over_aside_selftest(httrackp *opt, const char *src,
|
||||
const char *dst);
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ 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"
|
||||
@@ -1114,7 +1115,7 @@ static void warc_wacz_package(warc_writer *w) {
|
||||
hts_log_print(w->opt, LOG_WARNING,
|
||||
"WACZ: packaging failed, kept existing %s untouched",
|
||||
waczpath);
|
||||
} else if (!hts_rename_over(tmppath, waczpath)) {
|
||||
} else if (!hts_rename_over(w->opt, tmppath, waczpath)) {
|
||||
(void) UNLINK(fconv(catbuff, sizeof(catbuff), tmppath));
|
||||
hts_log_print(w->opt, LOG_WARNING | LOG_ERRNO,
|
||||
"WACZ: could not finalize %s", waczpath);
|
||||
@@ -1354,6 +1355,7 @@ 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;
|
||||
}
|
||||
@@ -1589,7 +1591,7 @@ static hts_boolean warc_commit(warc_writer *w) {
|
||||
for (s = 0; s < nseg; s++) {
|
||||
const char *final = warc_seg_path(w, s, finalbuf, sizeof(finalbuf));
|
||||
snprintf(tmpbuf, sizeof(tmpbuf), "%s" WARC_TMP_SUFFIX, final);
|
||||
if (!hts_rename_over(tmpbuf, final)) {
|
||||
if (!hts_rename_over(w->opt, tmpbuf, final)) {
|
||||
hts_log_print(w->opt, LOG_ERROR | LOG_ERRNO,
|
||||
"WARC: could not replace %s", final);
|
||||
return HTS_FALSE;
|
||||
|
||||
@@ -903,11 +903,10 @@ int PT_LoadCache__New(PT_Index index_, const char *filename) {
|
||||
coucal hashtable = index->hash;
|
||||
|
||||
/* Compute base path for this index - the filename MUST be absolute! */
|
||||
for (slashes = 2, abpath = hts_lastcharptr(filename);
|
||||
abpath > filename &&
|
||||
((*abpath != '/' && *abpath != '\\') || --slashes > 0);
|
||||
abpath--)
|
||||
;
|
||||
for(slashes = 2, abpath = filename + (int) strlen(filename) - 1;
|
||||
abpath > filename && ((*abpath != '/' && *abpath != '\\')
|
||||
|| --slashes > 0);
|
||||
abpath--) ;
|
||||
index->path[0] = '\0';
|
||||
if (slashes == 0 && *abpath != 0) {
|
||||
int i;
|
||||
@@ -1500,11 +1499,10 @@ static int PT_LoadCache__Old(PT_Index index_, const char *filename) {
|
||||
|
||||
/* -------------------- COPY OF THE __New() CODE -------------------- */
|
||||
/* Compute base path for this index - the filename MUST be absolute! */
|
||||
for (slashes = 2, abpath = hts_lastcharptr(filename);
|
||||
abpath > filename &&
|
||||
((*abpath != '/' && *abpath != '\\') || --slashes > 0);
|
||||
abpath--)
|
||||
;
|
||||
for(slashes = 2, abpath = filename + (int) strlen(filename) - 1;
|
||||
abpath > filename && ((*abpath != '/' && *abpath != '\\')
|
||||
|| --slashes > 0);
|
||||
abpath--) ;
|
||||
index->path[0] = '\0';
|
||||
if (slashes == 0 && *abpath != 0) {
|
||||
int i;
|
||||
|
||||
@@ -4,4 +4,5 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
httrack -O /dev/null -#test=addlink | grep -q "addlink self-test OK"
|
||||
out=$(httrack -O /dev/null -#test=addlink)
|
||||
grep -q "addlink self-test OK" <<<"$out"
|
||||
|
||||
@@ -5,4 +5,5 @@ set -euo pipefail
|
||||
|
||||
# A ready slot still owning a temporary must stay in memory: swapping it out
|
||||
# clears the entry, which unlinks the re-fetch backup (#771).
|
||||
httrack -O /dev/null -#test=backswap | grep -q "backswap self-test: OK"
|
||||
out=$(httrack -O /dev/null -#test=backswap)
|
||||
grep -q "backswap self-test: OK" <<<"$out"
|
||||
|
||||
@@ -5,4 +5,5 @@ set -euo pipefail
|
||||
|
||||
# webhttrack posts its command line as one string: the argv split must grow past
|
||||
# 1024 arguments and keep a quote inside a value out of the option parser.
|
||||
httrack -O /dev/null -#test=cmdline-split run | grep -q "cmdline-split self-test OK"
|
||||
out=$(httrack -O /dev/null -#test=cmdline-split run)
|
||||
grep -q "cmdline-split self-test OK" <<<"$out"
|
||||
|
||||
@@ -9,4 +9,5 @@ set -euo pipefail
|
||||
dir=$(mktemp -d)
|
||||
trap 'set +e; rm -rf "$dir"' EXIT
|
||||
|
||||
httrack -O /dev/null -#test=cookieimport "$dir" | grep -q "cookieimport:.*OK"
|
||||
out=$(httrack -O /dev/null -#test=cookieimport "$dir")
|
||||
grep -q "cookieimport:.*OK" <<<"$out"
|
||||
|
||||
@@ -8,4 +8,5 @@ set -euo pipefail
|
||||
dir=$(mktemp -d)
|
||||
trap 'set +e; rm -rf "$dir"' EXIT
|
||||
|
||||
httrack -O /dev/null -#test=direnum "$dir" | grep -q "direnum:.*OK"
|
||||
out=$(httrack -O /dev/null -#test=direnum "$dir")
|
||||
grep -q "direnum:.*OK" <<<"$out"
|
||||
|
||||
@@ -4,4 +4,5 @@
|
||||
set -euo pipefail
|
||||
|
||||
# HT_ADD_HTMLESCAPED* must reserve the escaper's worst case (6 for _full).
|
||||
httrack -O /dev/null -#test=escape-room run | grep -q "escape-room self-test OK"
|
||||
out=$(httrack -O /dev/null -#test=escape-room run)
|
||||
grep -q "escape-room self-test OK" <<<"$out"
|
||||
|
||||
@@ -50,7 +50,7 @@ httrack "file://$deep/index.html" -O "$mir" -%F "$footer" -q -s0 -%v0 \
|
||||
|
||||
# The crawled page must exist (proves the URL wasn't rejected for length, so the
|
||||
# footer path ran). Look under file/, not $mir, to skip the makeindex top index.
|
||||
find "$mir/file" -name index.html | grep -q . || {
|
||||
test -n "$(find "$mir/file" -name index.html)" || {
|
||||
echo "page not mirrored; the oversized-footer path was not exercised" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -4,4 +4,5 @@
|
||||
set -euo pipefail
|
||||
|
||||
# get_ftp_line bounds a hostile CRLF-less FTP reply into its 1024-byte buffer.
|
||||
httrack -O /dev/null -#test=ftp-line run | grep -q "ftp-line self-test OK"
|
||||
out=$(httrack -O /dev/null -#test=ftp-line run)
|
||||
grep -q "ftp-line self-test OK" <<<"$out"
|
||||
|
||||
@@ -4,4 +4,5 @@
|
||||
set -euo pipefail
|
||||
|
||||
# ftp_split_userpass bounds an over-long user:pass@ from a hostile ftp:// URL.
|
||||
httrack -O /dev/null -#test=ftp-userpass run | grep -q "ftp-userpass self-test OK"
|
||||
out=$(httrack -O /dev/null -#test=ftp-userpass run)
|
||||
grep -q "ftp-userpass self-test OK" <<<"$out"
|
||||
|
||||
@@ -19,7 +19,7 @@ printf -- '-*/zzmarker*\n' >"$tmp/rules.txt"
|
||||
# the rules file lands in the URL/filter string, echoed back by the banner
|
||||
run=$(httrack -O "$tmp/out" --quiet -n "-%S" "$tmp/rules.txt" \
|
||||
"file://$tmp/index.html" 2>&1) || true
|
||||
printf '%s\n' "$run" | grep -q 'zzmarker' || {
|
||||
grep -q 'zzmarker' <<<"$run" || {
|
||||
echo "FAIL: -%S rules file was not loaded"
|
||||
printf '%s\n' "$run"
|
||||
exit 1
|
||||
|
||||
@@ -6,4 +6,4 @@ set -euo pipefail
|
||||
# httrack internal hashtable autotest on 100K keys. Assert the success line (on
|
||||
# stderr) so a misrouted registry entry can't pass on exit code alone.
|
||||
out=$(httrack -#test=hashtable 100000 2>&1)
|
||||
printf '%s\n' "$out" | grep -q "all hashtable tests were successful!" || exit 1
|
||||
grep -q "all hashtable tests were successful!" <<<"$out" || exit 1
|
||||
|
||||
@@ -4,4 +4,5 @@
|
||||
set -euo pipefail
|
||||
|
||||
# inplace_escape_*() must match escape_*() on a copy: guards the shared helper.
|
||||
httrack -O /dev/null -#test=inplace-escape run | grep -q "inplace-escape self-test OK"
|
||||
out=$(httrack -O /dev/null -#test=inplace-escape run)
|
||||
grep -q "inplace-escape self-test OK" <<<"$out"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Runs the lastchar self-test (#770, #781), then keeps the idiom it replaced
|
||||
# from coming back: x[strlen(x) - 1] and x + strlen(x) - 1 both land one byte
|
||||
# before the buffer when x is empty, and the guard is never where they are.
|
||||
# Runs the lastchar self-test (#770), then keeps the idiom it replaced from
|
||||
# coming back: x[strlen(x) - 1] indexes one byte before the buffer when x is
|
||||
# empty, and the guard is never where the index is.
|
||||
|
||||
set -eu
|
||||
|
||||
@@ -33,36 +33,3 @@ test -z "$hits" || {
|
||||
echo "$hits" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Same for the pointer spelling. The three survivors sit in commented-out
|
||||
# blocks in htsname.c; the rest are this class being named, plus the self-test's
|
||||
# own deliberate underflow.
|
||||
hits=$(command grep -rnE '\+ *\(?(int|size_t)?\)? *strlen *\([^)]*\) *- *1' \
|
||||
"$top_srcdir/src" --include='*.c' --include='*.h' --exclude-dir=coucal |
|
||||
grep -v 'htsname\.c:.*char\* a=\(fil+strlen(fil)\|save+strlen(save)\)-1;' |
|
||||
grep -v 'htsselftest\.c:.*pointer spelling x + strlen(x) - 1' |
|
||||
grep -v 'htsselftest\.c:.*\*(s + strlen(s) - 1) =' || true)
|
||||
|
||||
test -z "$hits" || {
|
||||
echo "x + strlen(x) - 1 reintroduced; use hts_lastcharptr:" >&2
|
||||
echo "$hits" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# url_savename() has nine of these sites and reaches all of them with an empty
|
||||
# "fil", which "?x=1" produces. Names are asserted loosely, since the point is
|
||||
# the run: on unfixed code the sanitized CI leg aborts inside htsname.c.
|
||||
for args in "?x=1 text/plain" "?x=1 text/html" "?x=1 text/html type=4"; do
|
||||
# shellcheck disable=SC2086
|
||||
name=$(httrack -#test=savename $args) || {
|
||||
echo "httrack -#test=savename $args exited non-zero: $name" >&2
|
||||
exit 1
|
||||
}
|
||||
case "$name" in
|
||||
"savename: "?*) ;;
|
||||
*)
|
||||
echo "-#test=savename $args produced no name: $name" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
@@ -4,4 +4,5 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
httrack -O /dev/null -#test=logcallback | grep -q "logcallback self-test OK"
|
||||
out=$(httrack -O /dev/null -#test=logcallback)
|
||||
grep -q "logcallback self-test OK" <<<"$out"
|
||||
|
||||
@@ -8,4 +8,5 @@ set -euo pipefail
|
||||
dir=$(mktemp -d)
|
||||
trap 'set +e; rm -rf "$dir"' EXIT
|
||||
|
||||
httrack -O /dev/null -#test=longpath "$dir" | grep -q "longpath:.*OK"
|
||||
out=$(httrack -O /dev/null -#test=longpath "$dir")
|
||||
grep -q "longpath:.*OK" <<<"$out"
|
||||
|
||||
@@ -9,4 +9,5 @@ set -euo pipefail
|
||||
dir=$(mktemp -d)
|
||||
trap 'set +e; rm -rf "$dir"' EXIT
|
||||
|
||||
httrack -O /dev/null -#test=mirrorio "$dir" | grep -q "mirrorio:.*OK"
|
||||
out=$(httrack -O /dev/null -#test=mirrorio "$dir")
|
||||
grep -q "mirrorio:.*OK" <<<"$out"
|
||||
|
||||
@@ -6,4 +6,5 @@ set -euo pipefail
|
||||
# #159: a redirect to a same-file alias (http<->https, user@host, ..) must be
|
||||
# followed through, not turned into a self-pointing "moved" stub. The decision
|
||||
# helper is exercised by the engine self-test.
|
||||
httrack -O /dev/null -#test=redirect-samefile run | grep -q "redirect-samefile self-test OK"
|
||||
out=$(httrack -O /dev/null -#test=redirect-samefile run)
|
||||
grep -q "redirect-samefile self-test OK" <<<"$out"
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
set -euo pipefail
|
||||
|
||||
# Drives -#test=renameover: hts_rename_over() must replace an existing dst, and
|
||||
# must leave dst alone when the rename failed for a reason removing dst cannot
|
||||
# fix (#779). The selftest prints the regime it detected; pin it per platform so
|
||||
# a leg cannot pass having tested the other half.
|
||||
# must leave dst alone when the rename failed for a reason moving dst aside
|
||||
# cannot fix (#779, #790). The selftest prints the regime and the restore
|
||||
# outcome it took; pin both per leg so none can pass having tested another.
|
||||
dir=$(mktemp -d)
|
||||
trap 'set +e; rm -rf "$dir"' EXIT
|
||||
|
||||
@@ -16,8 +16,9 @@ MINGW* | MSYS_NT*) want=fallback ;; # native rename() refuses an existing target
|
||||
esac
|
||||
|
||||
out=$(httrack -O /dev/null -#test=renameover "$dir")
|
||||
echo "$out" | grep -q "renameover: OK"
|
||||
echo "$out" | grep -q "renameover: regime $want"
|
||||
grep -q "renameover: OK" <<<"$out"
|
||||
grep -q "renameover: regime $want" <<<"$out"
|
||||
grep -q "renameover: restore back" <<<"$out"
|
||||
|
||||
if [ "$(uname -s)" != "Linux" ]; then
|
||||
echo "renameover: LD_PRELOAD interposition is Linux-only here, skipping"
|
||||
@@ -42,14 +43,29 @@ fi
|
||||
# refuses by default. The shim allocates nothing, so the ordering is harmless.
|
||||
export ASAN_OPTIONS="${ASAN_OPTIONS:+$ASAN_OPTIONS:}verify_asan_link_order=0"
|
||||
|
||||
# The unlink fallback is dead code on POSIX, so borrow Windows' rename().
|
||||
# The aside fallback is dead code on POSIX, so borrow Windows' rename().
|
||||
out=$(LD_PRELOAD="$RENAMEFAIL_LIB" httrack -O /dev/null \
|
||||
-#test=renameover "$dir")
|
||||
echo "$out" | grep -q "renameover: OK"
|
||||
echo "$out" | grep -q "renameover: regime fallback"
|
||||
grep -q "renameover: OK" <<<"$out"
|
||||
grep -q "renameover: regime fallback" <<<"$out"
|
||||
grep -q "renameover: restore back" <<<"$out"
|
||||
|
||||
# #790: the move back out of the parked name fails once. Without the retry the
|
||||
# old copy stays parked and dst is left absent.
|
||||
out=$(RENAMEFAIL_ASIDE_FAILS=1 LD_PRELOAD="$RENAMEFAIL_LIB" httrack -O /dev/null \
|
||||
-#test=renameover "$dir")
|
||||
grep -q "renameover: OK" <<<"$out"
|
||||
grep -q "renameover: restore back" <<<"$out"
|
||||
|
||||
# It keeps failing: the old copy must survive under the parked name, never be
|
||||
# deleted, and the call must still report failure.
|
||||
out=$(RENAMEFAIL_ASIDE_FAILS=9 LD_PRELOAD="$RENAMEFAIL_LIB" httrack -O /dev/null \
|
||||
-#test=renameover "$dir")
|
||||
grep -q "renameover: OK" <<<"$out"
|
||||
grep -q "renameover: restore parked" <<<"$out"
|
||||
|
||||
# A source another process holds fails with EACCES, which dst had no part in.
|
||||
out=$(RENAMEFAIL_MODE=locked LD_PRELOAD="$RENAMEFAIL_LIB" httrack -O /dev/null \
|
||||
-#test=renameover "$dir")
|
||||
echo "$out" | grep -q "renameover: OK"
|
||||
echo "$out" | grep -q "renameover: regime refused"
|
||||
grep -q "renameover: OK" <<<"$out"
|
||||
grep -q "renameover: regime refused" <<<"$out"
|
||||
|
||||
@@ -4,4 +4,5 @@
|
||||
set -euo pipefail
|
||||
|
||||
# robots.txt RFC 9309 Allow/Disallow precedence (#452): longest match wins.
|
||||
httrack -O /dev/null -#test=robots run | grep -q "robots self-test OK"
|
||||
out=$(httrack -O /dev/null -#test=robots run)
|
||||
grep -q "robots self-test OK" <<<"$out"
|
||||
|
||||
@@ -165,3 +165,20 @@ 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'
|
||||
|
||||
@@ -7,11 +7,11 @@ set -eu
|
||||
|
||||
# Bare -#test lists known tests (printed to stderr).
|
||||
list=$(httrack -#test 2>&1)
|
||||
printf '%s\n' "$list" | grep -q "filter" || exit 1
|
||||
printf '%s\n' "$list" | grep -q "cache-writefail" || exit 1
|
||||
grep -q "filter" <<<"$list" || exit 1
|
||||
grep -q "cache-writefail" <<<"$list" || exit 1
|
||||
|
||||
# Unknown name: non-zero exit + diagnostic, and no test result line.
|
||||
rc=0
|
||||
err=$(httrack -#test=bogus 2>&1) || rc=$?
|
||||
test "$rc" -ne 0 || exit 1
|
||||
printf '%s\n' "$err" | grep -q "Unknown self-test" || exit 1
|
||||
grep -q "Unknown self-test" <<<"$err" || exit 1
|
||||
|
||||
@@ -6,4 +6,5 @@ set -euo pipefail
|
||||
# The SOCKS5 handshake framing and credential split, driven against scripted
|
||||
# server replies (frame draining, oversize rejects, RFC 1929 fields).
|
||||
|
||||
httrack -O /dev/null '-#test=socks5' | grep -q "socks5 self-test OK"
|
||||
out=$(httrack -O /dev/null '-#test=socks5')
|
||||
grep -q "socks5 self-test OK" <<<"$out"
|
||||
|
||||
@@ -4,4 +4,5 @@
|
||||
set -euo pipefail
|
||||
|
||||
# HTTP status -> reason phrase, including the modern 429/451 (#453).
|
||||
httrack -O /dev/null -#test=status run | grep -q "status self-test OK"
|
||||
out=$(httrack -O /dev/null -#test=status run)
|
||||
grep -q "status self-test OK" <<<"$out"
|
||||
|
||||
@@ -5,4 +5,5 @@ set -euo pipefail
|
||||
|
||||
# --strip-query: pattern-scoped query-key stripping for dedup. All assertions
|
||||
# live in the engine self-test (hts_query_strip_keys + fil_normalized_filtered).
|
||||
httrack -O /dev/null -#test=stripquery | grep -q "strip-query self-test OK"
|
||||
out=$(httrack -O /dev/null -#test=stripquery)
|
||||
grep -q "strip-query self-test OK" <<<"$out"
|
||||
|
||||
@@ -4,4 +4,5 @@
|
||||
set -euo pipefail
|
||||
|
||||
# Entity/URL unescapers reserve one byte for the trailing NUL (no 1-byte OOB).
|
||||
httrack -O /dev/null -#test=unescape-bounds run | grep -q "unescape-bounds self-test OK"
|
||||
out=$(httrack -O /dev/null -#test=unescape-bounds run)
|
||||
grep -q "unescape-bounds self-test OK" <<<"$out"
|
||||
|
||||
@@ -5,4 +5,5 @@ set -euo pipefail
|
||||
|
||||
# -%u url-hack split (#271): www / // / query-order dedup toggle independently.
|
||||
# All assertions live in the engine self-test (hash compare flag resolution).
|
||||
httrack -O /dev/null -#test=urlhack run | grep -q "urlhack self-test OK"
|
||||
out=$(httrack -O /dev/null -#test=urlhack run)
|
||||
grep -q "urlhack self-test OK" <<<"$out"
|
||||
|
||||
@@ -4,4 +4,5 @@
|
||||
set -euo pipefail
|
||||
|
||||
# Default User-Agent (#449): honest HTTrack token, no Windows 98 relic.
|
||||
httrack -O /dev/null -#test=useragent run | grep -q "useragent self-test OK"
|
||||
out=$(httrack -O /dev/null -#test=useragent run)
|
||||
grep -q "useragent self-test OK" <<<"$out"
|
||||
|
||||
@@ -5,4 +5,5 @@ set -euo pipefail
|
||||
|
||||
# SURT canonicalization of the CDXJ sort key (--warc-cdx). Pure string work,
|
||||
# so it runs under the MSan-instrumented 01_engine glob.
|
||||
httrack -O /dev/null -#test=warc-surt | grep -q "warc-surt: OK"
|
||||
out=$(httrack -O /dev/null -#test=warc-surt)
|
||||
grep -q "warc-surt: OK" <<<"$out"
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Keep this POSIX-portable: the harness runs it via $(BASH), which is a plain
|
||||
# POSIX /bin/sh on some platforms (e.g. macOS), so avoid bashisms and GNU-only
|
||||
# tool flags despite the #!/bin/bash above.
|
||||
# Stick to POSIX tool flags: macOS ships BSD grep/sed, not the GNU ones.
|
||||
|
||||
# Cache write-failure policy (-#test=cache-writefail <dir>). #174/#219: disk
|
||||
# full or a failure streak aborts cleanly; an isolated failure or an oversized
|
||||
@@ -17,13 +15,13 @@ out=$(httrack -#test=cache-writefail "$dir")
|
||||
|
||||
# Match the exact success line (error logs also go to stdout); a renamed/removed
|
||||
# test prints the registry to stderr, which exits non-zero but never prints this.
|
||||
printf '%s\n' "$out" | grep -qx "cache-writefail: OK" || {
|
||||
grep -qx "cache-writefail: OK" <<<"$out" || {
|
||||
echo "expected 'cache-writefail: OK', got: $out" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# A skipped entry must be warned about with its URL.
|
||||
printf '%s\n' "$out" | grep -q "entry not cached: example.com/" || {
|
||||
grep -q "entry not cached: example.com/" <<<"$out" || {
|
||||
echo "expected a URL-bearing skip warning" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Keep this POSIX-portable: the harness runs it via $(BASH), which is a plain
|
||||
# POSIX /bin/sh on some platforms (e.g. macOS), so avoid bashisms and GNU-only
|
||||
# tool flags despite the #!/bin/bash above.
|
||||
# Stick to POSIX tool flags: macOS ships BSD grep/sed, not the GNU ones.
|
||||
|
||||
# unzRepair header read must not overflow a signed shift (-#test=zip-repair-shift
|
||||
# <dir>). A damaged local file header whose CRC high word has bit 15 set made
|
||||
@@ -15,7 +13,7 @@ trap 'set +e; rm -rf "$dir"' EXIT
|
||||
|
||||
out=$(httrack -#test=zip-repair-shift "$dir")
|
||||
|
||||
printf '%s\n' "$out" | grep -qx "zip-repair-shift: OK (recovered 1 entry)" || {
|
||||
grep -qx "zip-repair-shift: OK (recovered 1 entry)" <<<"$out" || {
|
||||
echo "expected 'zip-repair-shift: OK (recovered 1 entry)', got: $out" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ set -euo pipefail
|
||||
|
||||
httrack_bin=$(cd "$(dirname "$(command -v httrack)")" && pwd)/httrack
|
||||
|
||||
if ! "$httrack_bin" -#test 2>&1 | grep -q '^ warc-wacz'; then
|
||||
registry=$("$httrack_bin" -#test 2>&1 || true)
|
||||
if ! grep -q '^ warc-wacz' <<<"$registry"; then
|
||||
echo "warc-wacz self-test unavailable (build without OpenSSL); skipping"
|
||||
exit 77
|
||||
fi
|
||||
|
||||
@@ -12,8 +12,8 @@ set -euo pipefail
|
||||
: "${top_srcdir:=..}"
|
||||
|
||||
bash "$top_srcdir/tests/local-crawl.sh" \
|
||||
--plant-file bigtrunc/slow.bin.bak \
|
||||
--plant-dir bigtrunc/fast.bin.bak \
|
||||
--plant-file bigtrunc/hts-tmp/slow.bin.bak \
|
||||
--plant-dir bigtrunc/hts-tmp/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' \
|
||||
|
||||
@@ -152,13 +152,15 @@ ok "the kept copies survived the update purge"
|
||||
# --- the change report is the mirror's own account of the pass ----------------
|
||||
test -s "$report" || fail "pass 2 wrote no ${report}"
|
||||
test -z "$(listed gone)" || fail "the report calls something gone: $(listed gone)"
|
||||
unchanged=$(listed unchanged)
|
||||
changed=$(listed changed)
|
||||
for name in keep empty; do
|
||||
listed unchanged | grep -qx "${host}/${name}.bin:$(size_of "${tmpdir}/snap/${name}.bin")" ||
|
||||
grep -qx "${host}/${name}.bin:$(size_of "${tmpdir}/snap/${name}.bin")" <<<"$unchanged" ||
|
||||
fail "${name}.bin is not reported unchanged at its previous size"
|
||||
if listed changed | grep -q "^${host}/${name}.bin:"; then
|
||||
if grep -q "^${host}/${name}.bin:" <<<"$changed"; then
|
||||
fail "${name}.bin is reported changed as well"
|
||||
fi
|
||||
done
|
||||
listed changed | grep -qx "${host}/stay.bin:$(size_of "${out}/${host}/stay.bin")" ||
|
||||
grep -qx "${host}/stay.bin:$(size_of "${out}/${host}/stay.bin")" <<<"$changed" ||
|
||||
fail "stay.bin is not reported changed"
|
||||
ok "the change report calls the kept files unchanged and the refreshed one changed"
|
||||
|
||||
173
tests/106_engine-repair-rename.test
Executable file
173
tests/106_engine-repair-rename.test
Executable file
@@ -0,0 +1,173 @@
|
||||
#!/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"
|
||||
21
tests/107_local-bak-collision.test
Executable file
21
tests/107_local-bak-collision.test
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/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'
|
||||
25
tests/108_engine-refetch-backup.test
Executable file
25
tests/108_engine-refetch-backup.test
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/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
|
||||
}
|
||||
125
tests/111_local-ftp-update-rest.test
Normal file
125
tests/111_local-ftp-update-rest.test
Normal file
@@ -0,0 +1,125 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# FTP resumed with REST whenever the mirrored file merely existed, so an
|
||||
# --update over a complete copy spliced the old body into the new one and the
|
||||
# result matched the remote length (#798). Pass 1 is cut short to leave a real
|
||||
# partial, pass 2 must still resume it, pass 3 must not.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
: "${top_srcdir:=..}"
|
||||
testdir=$(cd "$(dirname "$0")" && pwd)
|
||||
# shellcheck source=tests/testlib.sh
|
||||
. "${testdir}/testlib.sh"
|
||||
|
||||
python=$(find_python) || ! echo "python3 not found; skipping" >&2 || exit 77
|
||||
command -v httrack >/dev/null || {
|
||||
echo "could not find httrack" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
server=$(nativepath "${testdir}/ftp-server.py")
|
||||
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/httrack_ftp.XXXXXX")
|
||||
serverpid=
|
||||
cleanup() {
|
||||
stop_server "$serverpid"
|
||||
rm -rf "$tmpdir"
|
||||
}
|
||||
trap 'set +e; cleanup' EXIT
|
||||
trap cleanup HUP INT QUIT PIPE TERM
|
||||
|
||||
root="${tmpdir}/root"
|
||||
out="${tmpdir}/crawl"
|
||||
mode="${tmpdir}/mode"
|
||||
cmds="${tmpdir}/cmds"
|
||||
mkdir -p "$root" "$out"
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
ok() { echo "OK: $*"; }
|
||||
size_of() { wc -c <"$1" | tr -d '[:space:]'; }
|
||||
# hts-log.txt is rewritten per pass, so this only ever sees the pass just run.
|
||||
no_errors() {
|
||||
local errors
|
||||
errors=$(grep -a 'Error:' "${out}/hts-log.txt" || true)
|
||||
test -z "$errors" || fail "$1 reported errors: ${errors}"
|
||||
}
|
||||
# Commands the server saw since the last pass; each pass starts by emptying it.
|
||||
sent() { cat "$cmds"; }
|
||||
|
||||
# The two generations differ in length and in their first bytes, so a splice
|
||||
# shows up whichever end of the file it lands in.
|
||||
write_body() {
|
||||
"$python" -c 'import sys; sys.stdout.buffer.write(
|
||||
("%s-BODY " % sys.argv[1]).encode() + sys.argv[1][:1].encode() * int(sys.argv[2]))' \
|
||||
"$1" "$2" >"${root}/a.bin"
|
||||
}
|
||||
write_body OLD 8000
|
||||
echo '/a.bin truncate' >"$mode"
|
||||
|
||||
serverlog="${tmpdir}/server.out"
|
||||
"$python" "$server" --root "$(nativepath "$root")" \
|
||||
--mode-file "$(nativepath "$mode")" --log "$(nativepath "$cmds")" \
|
||||
>"$serverlog" 2>&1 &
|
||||
serverpid=$!
|
||||
port=
|
||||
for _ in $(seq 1 300); do
|
||||
line=$(grep -m1 '^PORT ' "$serverlog" 2>/dev/null) && port="${line#PORT }" && break
|
||||
kill -0 "$serverpid" 2>/dev/null || {
|
||||
echo "ftp server exited early: $(cat "$serverlog")" >&2
|
||||
exit 1
|
||||
}
|
||||
sleep 0.1
|
||||
done
|
||||
test -n "$port" || {
|
||||
echo "could not discover ftp server port: $(cat "$serverlog")" >&2
|
||||
exit 1
|
||||
}
|
||||
host="127.0.0.1_${port}"
|
||||
url="ftp://127.0.0.1:${port}/a.bin"
|
||||
mirror="${out}/${host}/a.bin"
|
||||
# Bounded like every crawl pass: a wedge must fail the test, not hang the job.
|
||||
crawl() { run_with_timeout 300 httrack "$url" -O "$out" --quiet \
|
||||
--disable-security-limits --robots=0 --timeout=20 --max-time=120 --retries=1 \
|
||||
-c1 "$@"; }
|
||||
|
||||
# --- pass 1: the transfer dies mid-body and leaves a partial -----------------
|
||||
crawl >"${tmpdir}/log1" 2>&1 || true
|
||||
test -f "$mirror" || fail "pass 1 mirrored nothing to resume"
|
||||
partial=$(size_of "$mirror")
|
||||
whole=$(size_of "${root}/a.bin")
|
||||
if test "$partial" -le 0 || test "$partial" -ge "$whole"; then
|
||||
fail "pass 1 left ${partial} bytes, wanted a partial of ${whole}"
|
||||
fi
|
||||
ok "pass 1 left a ${partial}-byte partial copy"
|
||||
|
||||
# --- pass 2: the partial is resumed ------------------------------------------
|
||||
: >"$mode"
|
||||
: >"$cmds"
|
||||
crawl >"${tmpdir}/log2" 2>&1
|
||||
case "$(sent)" in
|
||||
*"REST ${partial}"*) ok "pass 2 resumed at the partial's ${partial} bytes" ;;
|
||||
*) fail "pass 2 sent no REST ${partial}; commands were: $(sent)" ;;
|
||||
esac
|
||||
cmp -s "$mirror" "${root}/a.bin" ||
|
||||
fail "the resumed copy is $(size_of "$mirror") bytes, served $(size_of "${root}/a.bin")"
|
||||
# A resume used to count only the appended tail against the remote size, so it
|
||||
# was reported "FTP file incomplete" even when it produced the whole file.
|
||||
no_errors "pass 2"
|
||||
ok "the resumed transfer produced the served body and no error"
|
||||
|
||||
# --- pass 3: --update over that complete copy --------------------------------
|
||||
write_body NEW 12000
|
||||
: >"$cmds"
|
||||
crawl --update >"${tmpdir}/log3" 2>&1
|
||||
case "$(sent)" in
|
||||
*REST*) fail "the update resumed a complete mirror: $(sent)" ;;
|
||||
*RETR*) ok "the update re-fetched without REST" ;;
|
||||
*) fail "pass 3 never reached RETR; commands were: $(sent)" ;;
|
||||
esac
|
||||
cmp -s "$mirror" "${root}/a.bin" ||
|
||||
fail "the updated copy is $(size_of "$mirror") bytes, served $(size_of "${root}/a.bin")"
|
||||
no_errors "pass 3"
|
||||
ok "the update replaced the mirror with the new body"
|
||||
@@ -1,107 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Backward scans that start at s + strlen(s) - 1 and decrement with no lower
|
||||
# bound: the config-file right trim and the collision-rename digit scan both
|
||||
# walked off the front of a stack buffer. Neither ASan nor _FORTIFY_SOURCE sees
|
||||
# the self-test's overrun (it lands in the same frame), hence the poisoned byte.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
: "${top_srcdir:=..}"
|
||||
|
||||
# Resolve httrack before any cd: PATH may carry a build-relative entry.
|
||||
bin=$(command -v httrack) || {
|
||||
echo "FAIL: httrack not found on PATH" >&2
|
||||
exit 1
|
||||
}
|
||||
case "$bin" in
|
||||
/*) ;;
|
||||
*) bin="$(cd "$(dirname "$bin")" && pwd)/$(basename "$bin")" ;;
|
||||
esac
|
||||
|
||||
tmp=$(mktemp -d "${TMPDIR:-/tmp}/httrack_rtrim.XXXXXX") || exit 1
|
||||
trap 'set +e; rm -rf "$tmp"' EXIT
|
||||
trap 'rm -rf "$tmp"' HUP INT QUIT PIPE TERM
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- 1. the helper itself ---------------------------------------------------
|
||||
out=$("$bin" -#test=rtrim) || fail "httrack -#test=rtrim exited non-zero: $out"
|
||||
test "$out" = "rtrim self-test: OK" || fail "expected 'rtrim self-test: OK', got: $out"
|
||||
|
||||
# --- 2. collision rename over an all-digit name -----------------------------
|
||||
# Two pages with the same all-digit name save to 2024.html, so the second goes
|
||||
# through the -<n> rename and the digit scan has nothing but digits to walk
|
||||
# over. Driven twice, because the two routes differ in who picks the URLs.
|
||||
site="$tmp/site"
|
||||
mkdir -p "$site/a" "$site/b"
|
||||
printf '%s\n' '<html><body><a href="a/2024.html">a</a> <a href="b/2024.html">b</a></body></html>' \
|
||||
>"$site/index.html"
|
||||
echo '<html><body>A</body></html>' >"$site/a/2024.html"
|
||||
echo '<html><body>B</body></html>' >"$site/b/2024.html"
|
||||
|
||||
# -g flattens the tree, and pins depth to 0: the operator supplies both URLs.
|
||||
out1="$tmp/out1"
|
||||
mkdir -p "$out1"
|
||||
(cd "$out1" && "$bin" -g "file://$site/a/2024.html" "file://$site/b/2024.html" \
|
||||
--quiet -n >.log 2>&1) || fail "collision crawl exited non-zero: $(tail -3 "$out1/.log")"
|
||||
|
||||
test -f "$out1/2024.html" || fail "2024.html not mirrored"
|
||||
test -f "$out1/2024-2.html" || fail "colliding name was not renamed to 2024-2.html"
|
||||
|
||||
# -N "%n.%t" collides the same way with depth unrestricted, so one starting URL
|
||||
# is enough and the crawled page picks both colliding names itself.
|
||||
out3="$tmp/out3"
|
||||
mkdir -p "$out3"
|
||||
(cd "$out3" && "$bin" "file://$site/index.html" -N "%n.%t" --quiet -n >.log 2>&1) ||
|
||||
fail "userdef-savename crawl exited non-zero: $(tail -3 "$out3/.log")"
|
||||
test -f "$out3/2024.html" || fail "2024.html not mirrored under -N '%n.%t'"
|
||||
test -f "$out3/2024-2.html" ||
|
||||
fail "colliding name was not renamed under -N '%n.%t' (links followed, not argv)"
|
||||
|
||||
# The rename must keep counting, not restart: a third collision is -3.
|
||||
mkdir -p "$site/c"
|
||||
echo '<html><body>C</body></html>' >"$site/c/2024.html"
|
||||
out2="$tmp/out2"
|
||||
mkdir -p "$out2"
|
||||
(cd "$out2" && "$bin" -g "file://$site/a/2024.html" "file://$site/b/2024.html" \
|
||||
"file://$site/c/2024.html" --quiet -n >.log 2>&1) ||
|
||||
fail "three-way collision crawl exited non-zero: $(tail -3 "$out2/.log")"
|
||||
test -f "$out2/2024-3.html" || fail "third colliding name was not renamed to 2024-3.html"
|
||||
|
||||
# --- 3. a blank config line is skipped, not parsed ---------------------------
|
||||
# HTS_HTTRACKRC is ".httrackrc" on POSIX and "httrackrc" on Windows.
|
||||
rcdir="$tmp/rc"
|
||||
mkdir -p "$rcdir"
|
||||
echo '<html><body>hi</body></html>' >"$rcdir/index.html"
|
||||
printf ' \nzzz-not-an-option\n' >"$rcdir/.httrackrc"
|
||||
cp "$rcdir/.httrackrc" "$rcdir/httrackrc"
|
||||
|
||||
rc=0
|
||||
log=$( (cd "$rcdir" && "$bin" "file://$rcdir/index.html" --quiet -n 2>&1)) || rc=$?
|
||||
test "$rc" -eq 0 || fail "rc-file crawl exited $rc: $log"
|
||||
log=${log//$'\r'/} # the empty-option check below must not miss a CRLF line
|
||||
|
||||
# The line after the blank one must still be read, or this proves nothing.
|
||||
case "$log" in
|
||||
*"Unknown option: zzz-not-an-option"*) ;;
|
||||
*) fail "config file was not parsed past the blank line: $log" ;;
|
||||
esac
|
||||
# The blank line itself must not become an empty option.
|
||||
case "$log" in
|
||||
*"Unknown option: "$'\n'*) fail "blank config line was parsed as an empty option: $log" ;;
|
||||
esac
|
||||
|
||||
# --- 4. the call sites stay bounded -----------------------------------------
|
||||
# Sections 2 and 3 only go red under a sanitizer: on a plain build the byte
|
||||
# below the buffer is usually not a digit, so a reverted scan stops anyway and
|
||||
# produces the same names. Pin the two sites at the source instead.
|
||||
command grep -q 'hts_rtrim(line, HTS_REALSPACES)' "$top_srcdir/src/htsalias.c" ||
|
||||
fail "htsalias.c right trim no longer goes through hts_rtrim"
|
||||
command grep -q 'hts_rtrimlen(tempo, "0123456789")' "$top_srcdir/src/htsname.c" ||
|
||||
fail "htsname.c collision-suffix scan no longer goes through hts_rtrimlen"
|
||||
|
||||
exit 0
|
||||
@@ -13,7 +13,10 @@ bash "$top_srcdir/tests/local-crawl.sh" --errors 1 \
|
||||
--not-found 'errpage/missing.html' \
|
||||
httrack 'BASEURL/errpage/index.html' '-o0'
|
||||
|
||||
# Control -o1 (default): the 404 error page is written.
|
||||
# Control -o1 (default): the 404 body is the server's verbatim, no HTTrack marker (#787); good.html proves the marker check can fire.
|
||||
bash "$top_srcdir/tests/local-crawl.sh" --errors 1 \
|
||||
--found 'errpage/missing.html' \
|
||||
--file-matches 'errpage/missing.html' '404 error body' \
|
||||
--file-not-matches 'errpage/missing.html' 'HTTrack' \
|
||||
--file-matches 'errpage/good.html' 'HTTrack' \
|
||||
httrack 'BASEURL/errpage/index.html' '-o1'
|
||||
|
||||
@@ -23,7 +23,7 @@ funcs=$(sed -n '/^function lang_index/,/^}/p' "${script}")
|
||||
# would truncate the lift and silently stop testing everything below it.
|
||||
grep -q '^# Find the browser' "${script}" || fail "locale block terminator moved in ${script}"
|
||||
block=$(sed -n '/^# Locale/,/^# Find the browser/p' "${script}" | sed '$d')
|
||||
echo "${block}" | grep -q 'LANGN=.*lang_index' || fail "could not lift the whole locale block from ${script}"
|
||||
grep -q 'LANGN=.*lang_index' <<<"${block}" || fail "could not lift the whole locale block from ${script}"
|
||||
|
||||
# Run the lifted block against whatever locale vars the caller exported.
|
||||
run_block() {
|
||||
|
||||
@@ -9,4 +9,5 @@ set -euo pipefail
|
||||
# (#614) was stripped as if it were the default. The default is scheme-aware
|
||||
# (#638): an explicit :80 on https/ftp stays, :443/:21 strip under their own
|
||||
# scheme. All assertions live in the engine self-test.
|
||||
httrack -O /dev/null -#test=stripport | grep -q "stripport self-test OK"
|
||||
out=$(httrack -O /dev/null -#test=stripport)
|
||||
grep -q "stripport self-test OK" <<<"$out"
|
||||
|
||||
@@ -21,7 +21,7 @@ test "$rc" -ne 77 || {
|
||||
echo "SKIP: local crawl prerequisites missing" >&2
|
||||
exit 77
|
||||
}
|
||||
if printf '%s\n' "$out" | grep -q "could not discover server port"; then
|
||||
if grep -q "could not discover server port" <<<"$out"; then
|
||||
echo "SKIP: server port announce raced" >&2
|
||||
exit 77
|
||||
fi
|
||||
@@ -30,7 +30,7 @@ fi
|
||||
# together they pin the fast exit on the 3s watchdog, not httrack giving up.
|
||||
test "$rc" -ne 0 || fail "stalled crawl reported success"
|
||||
test "$elapsed" -lt 25 || fail "watchdog fired late (${elapsed}s)"
|
||||
printf '%s\n' "$out" | grep -q "watchdog fired" ||
|
||||
grep -q "watchdog fired" <<<"$out" ||
|
||||
fail "crawl failed but not via the watchdog: $out"
|
||||
|
||||
echo "crawl watchdog OK (reaped in ${elapsed}s)"
|
||||
|
||||
@@ -71,7 +71,8 @@ grep -qE 'st_strsafe|strcpy_safe_' "$oracle" || {
|
||||
# The payload. Dropping the raw frames first is what makes it specific: both
|
||||
# names are static, so .dynsym could never have carried them. Not anchored on
|
||||
# the "0xOFF:" line, the inline chain puts the name on either half.
|
||||
grep -v "$rawframe" "$out" | grep -qE 'st_strsafe|strcpy_safe_' || {
|
||||
symbolized=$(grep -v "$rawframe" "$out" || true)
|
||||
grep -qE 'st_strsafe|strcpy_safe_' <<<"$symbolized" || {
|
||||
echo "the handler did not name the hidden frames:" >&2
|
||||
cat "$out" >&2
|
||||
exit 1
|
||||
|
||||
@@ -169,7 +169,8 @@ test "${written}" -lt "${#profile}" ||
|
||||
fail "the file-size limit did not stop the write (${written} bytes landed)"
|
||||
check_clipped "Unable to write ${#profile} bytes in the the init file " "${path}/p"
|
||||
|
||||
get "${port}" /server/index.html | grep -q '200 OK' ||
|
||||
reply=$(get "${port}" /server/index.html || true)
|
||||
grep -q '200 OK' <<<"$reply" ||
|
||||
fail "the server stopped answering after the refused saves"
|
||||
|
||||
echo "PASS"
|
||||
|
||||
@@ -227,6 +227,9 @@ TESTS = \
|
||||
103_teardown-status.test \
|
||||
104_engine-warc-longurl.test \
|
||||
105_suite-timeout.test \
|
||||
116_engine-rtrim.test
|
||||
106_engine-repair-rename.test \
|
||||
107_local-bak-collision.test \
|
||||
108_engine-refetch-backup.test \
|
||||
111_local-ftp-update-rest.test
|
||||
|
||||
CLEANFILES = check-network_sh.cache
|
||||
|
||||
@@ -345,6 +345,7 @@ 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"
|
||||
@@ -529,9 +530,25 @@ if test -n "$wacz_validate"; then
|
||||
fi
|
||||
|
||||
# No crawl, even a cancelled one, may leave engine temporaries: .delayed (#107,
|
||||
# #483), or the .z/.u content-coding temps (#557).
|
||||
# #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.
|
||||
info "checking for leftover engine temporaries"
|
||||
leftovers=$(find "$out" \( -name '*.delayed' -o -name '*.z' -o -name '*.u' \) 2>/dev/null | head -5)
|
||||
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
|
||||
if test -z "$leftovers"; then result "OK"; else
|
||||
result "leftover: $leftovers"
|
||||
exit 1
|
||||
|
||||
@@ -1553,6 +1553,40 @@ 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
|
||||
@@ -2134,6 +2168,9 @@ 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,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
/* Borrows Windows' rename() for 01_engine-renameover.test, since POSIX cannot
|
||||
reach hts_rename_over()'s unlink fallback: an existing target is refused with
|
||||
/* Borrows Windows' rename() for the tests POSIX cannot otherwise reach:
|
||||
hts_rename_over()'s aside fallback needs an existing target refused with
|
||||
EEXIST, and RENAMEFAIL_MODE=locked reports EACCES instead, as the CRT does
|
||||
for a source another process holds. */
|
||||
for a source another process holds. =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). */
|
||||
|
||||
#define _GNU_SOURCE
|
||||
#include <dlfcn.h>
|
||||
@@ -18,15 +21,32 @@ 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 int locked = mode != NULL && strcmp(mode, "locked") == 0;
|
||||
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;
|
||||
struct stat st;
|
||||
|
||||
if (locked) {
|
||||
if (mode != NULL && (strcmp(mode, "locked") == 0 ||
|
||||
(repaired && strcmp(mode, "repair") == 0))) {
|
||||
errno = EACCES;
|
||||
return -1;
|
||||
}
|
||||
if (stat(newpath, &st) == 0) {
|
||||
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) {
|
||||
errno = EEXIST;
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -53,13 +53,13 @@ echo "stub browser invoked with: \$1" >&2
|
||||
opturl="\${1%/}/server/option2.html"
|
||||
warcurl="\${1%/}/server/option9.html"
|
||||
smurl="\${1%/}/server/option8.html"
|
||||
if body="\$(curl -fsSL --max-time 20 "\$1")" && printf '%s' "\$body" | grep -qai httrack && printf '%s' "\$body" | grep -qaF step2.html &&
|
||||
opt="\$(curl -fsSL --max-time 20 "\$opturl")" && printf '%s' "\$opt" | grep -qaF "title='" &&
|
||||
printf '%s' "\$opt" | grep -qaF 'name="singlefile"' && printf '%s' "\$opt" | grep -qaF 'name="singlefilemax"' &&
|
||||
! printf '%s' "\$opt" | grep -qaF '\${LANG_SINGLEFILE}' &&
|
||||
warc="\$(curl -fsSL --max-time 20 "\$warcurl")" && printf '%s' "\$warc" | grep -qaF 'name="warcfile"' && printf '%s' "\$warc" | grep -qaF WARC &&
|
||||
printf '%s' "\$warc" | grep -qaF 'name="changes"' && printf '%s' "\$warc" | grep -qaF hts-changes.json &&
|
||||
sm="\$(curl -fsSL --max-time 20 "\$smurl")" && printf '%s' "\$sm" | grep -qaF 'name="sitemapurl"' && printf '%s' "\$sm" | grep -qaF 'name="sitemap"'; then
|
||||
if body="\$(curl -fsSL --max-time 20 "\$1")" && grep -qai httrack <<<"\$body" && grep -qaF step2.html <<<"\$body" &&
|
||||
opt="\$(curl -fsSL --max-time 20 "\$opturl")" && grep -qaF "title='" <<<"\$opt" &&
|
||||
grep -qaF 'name="singlefile"' <<<"\$opt" && grep -qaF 'name="singlefilemax"' <<<"\$opt" &&
|
||||
! grep -qaF '\${LANG_SINGLEFILE}' <<<"\$opt" &&
|
||||
warc="\$(curl -fsSL --max-time 20 "\$warcurl")" && grep -qaF 'name="warcfile"' <<<"\$warc" && grep -qaF WARC <<<"\$warc" &&
|
||||
grep -qaF 'name="changes"' <<<"\$warc" && grep -qaF hts-changes.json <<<"\$warc" &&
|
||||
sm="\$(curl -fsSL --max-time 20 "\$smurl")" && grep -qaF 'name="sitemapurl"' <<<"\$sm" && grep -qaF 'name="sitemap"' <<<"\$sm"; then
|
||||
echo PASS >"$marker"
|
||||
else
|
||||
echo "FAIL: unexpected response from \$1" >"$marker"
|
||||
|
||||
Reference in New Issue
Block a user