mirror of
https://github.com/xroche/httrack.git
synced 2026-07-11 03:16:17 +03:00
Compare commits
10 Commits
fix-header
...
fix/180-ut
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84701fe609 | ||
|
|
c4e8c94b92 | ||
|
|
7faceaf7d2 | ||
|
|
9105732112 | ||
|
|
9c01812141 | ||
|
|
7a02d5e411 | ||
|
|
3c7e9fa46d | ||
|
|
a707d4b845 | ||
|
|
65c1016a4e | ||
|
|
fd2252dc8c |
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@@ -43,6 +43,12 @@ jobs:
|
||||
languages: c-cpp
|
||||
build-mode: manual
|
||||
queries: security-extended
|
||||
# fopen's umask-controlled 0666 is intended for mirror/cache/log
|
||||
# output; the one credential file (cookies.txt) is kept 0600 on Unix.
|
||||
config: |
|
||||
query-filters:
|
||||
- exclude:
|
||||
id: cpp/world-writable-file-creation
|
||||
|
||||
# Manual build: CodeQL traces the compiler, so build exactly what ships.
|
||||
- name: Build
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -34,8 +34,9 @@ Makefile
|
||||
*.so.*
|
||||
*.a
|
||||
|
||||
# make dist output.
|
||||
# make dist output; dist/ holds httrack-gh-release staging artifacts.
|
||||
/httrack-*.tar.gz
|
||||
/dist/
|
||||
|
||||
# Editor / autotools backup files.
|
||||
*~
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# libFuzzer harnesses; built only with --enable-fuzzers (requires clang).
|
||||
if FUZZERS
|
||||
noinst_PROGRAMS = fuzz-charset fuzz-meta fuzz-idna fuzz-entities \
|
||||
fuzz-unescape fuzz-filters fuzz-url
|
||||
fuzz-unescape fuzz-filters fuzz-url fuzz-header fuzz-cachendx
|
||||
endif
|
||||
|
||||
AM_CPPFLAGS = \
|
||||
@@ -23,6 +23,8 @@ fuzz_entities_SOURCES = fuzz-entities.c fuzz.h
|
||||
fuzz_unescape_SOURCES = fuzz-unescape.c fuzz.h
|
||||
fuzz_filters_SOURCES = fuzz-filters.c fuzz.h
|
||||
fuzz_url_SOURCES = fuzz-url.c fuzz.h
|
||||
fuzz_header_SOURCES = fuzz-header.c fuzz.h
|
||||
fuzz_cachendx_SOURCES = fuzz-cachendx.c fuzz.h
|
||||
|
||||
# List corpus files explicitly: automake does not expand EXTRA_DIST globs.
|
||||
EXTRA_DIST = README.md run-fuzzers.sh \
|
||||
@@ -34,5 +36,10 @@ EXTRA_DIST = README.md run-fuzzers.sh \
|
||||
corpus/unescape/percent.txt \
|
||||
corpus/filters/filter.bin corpus/filters/filter-size.bin \
|
||||
corpus/filters/regress-empty-subject-unique.bin \
|
||||
corpus/filters/redos-star-classes.bin \
|
||||
corpus/url/http-url.txt corpus/url/relative-path.txt \
|
||||
corpus/url/regress-file-empty-path.txt corpus/url/regress-long-path-abort.txt
|
||||
corpus/url/regress-file-empty-path.txt corpus/url/regress-long-path-abort.txt \
|
||||
corpus/header/full-response.txt corpus/header/redirect.txt \
|
||||
corpus/cachendx/new-format.txt corpus/cachendx/old-format.txt \
|
||||
corpus/cachendx/regress-overadvance.bin \
|
||||
corpus/cachendx/regress-truncated-entry.bin
|
||||
|
||||
7
fuzz/corpus/cachendx/new-format.txt
Normal file
7
fuzz/corpus/cachendx/new-format.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
8
|
||||
CACHE-1.1
|
||||
28
|
||||
Mon, 01 Jan 2024 00:00:00 GMT
|
||||
www.example.com
|
||||
/index.html
|
||||
123
|
||||
5
fuzz/corpus/cachendx/old-format.txt
Normal file
5
fuzz/corpus/cachendx/old-format.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
3
|
||||
1.0
|
||||
www.example.com
|
||||
/page
|
||||
5
|
||||
2
fuzz/corpus/cachendx/regress-overadvance.bin
Normal file
2
fuzz/corpus/cachendx/regress-overadvance.bin
Normal file
@@ -0,0 +1,2 @@
|
||||
32768
|
||||
CACHE-1.1
|
||||
5
fuzz/corpus/cachendx/regress-truncated-entry.bin
Normal file
5
fuzz/corpus/cachendx/regress-truncated-entry.bin
Normal file
@@ -0,0 +1,5 @@
|
||||
8
|
||||
CACHE-1.1
|
||||
1
|
||||
x
|
||||
www.example.com
|
||||
BIN
fuzz/corpus/filters/redos-star-classes.bin
Normal file
BIN
fuzz/corpus/filters/redos-star-classes.bin
Normal file
Binary file not shown.
10
fuzz/corpus/header/full-response.txt
Normal file
10
fuzz/corpus/header/full-response.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Content-Length: 1234
|
||||
Content-Encoding: gzip
|
||||
Last-Modified: Mon, 01 Jan 2024 00:00:00 GMT
|
||||
Etag: "abc"
|
||||
Location: http://example.com/x
|
||||
Set-Cookie: ID=42; path=/; domain=.example.com
|
||||
Content-Range: bytes 0-99/100
|
||||
Transfer-Encoding: chunked
|
||||
3
fuzz/corpus/header/redirect.txt
Normal file
3
fuzz/corpus/header/redirect.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
HTTP/1.0 301 Moved
|
||||
Location: /elsewhere
|
||||
Content-Disposition: attachment; filename="a.pdf"
|
||||
1
fuzz/corpus/meta/meta-content-first.html
Normal file
1
fuzz/corpus/meta/meta-content-first.html
Normal file
@@ -0,0 +1 @@
|
||||
<meta content="text/html; charset=gb2312" http-equiv="content-type">
|
||||
1
fuzz/corpus/meta/meta-truncated.html
Normal file
1
fuzz/corpus/meta/meta-truncated.html
Normal file
@@ -0,0 +1 @@
|
||||
<meta charset='utf-8
|
||||
1
fuzz/corpus/meta/meta-unquoted.html
Normal file
1
fuzz/corpus/meta/meta-unquoted.html
Normal file
@@ -0,0 +1 @@
|
||||
<meta charset = utf-8 />
|
||||
65
fuzz/fuzz-cachendx.c
Normal file
65
fuzz/fuzz-cachendx.c
Normal file
@@ -0,0 +1,65 @@
|
||||
/* ------------------------------------------------------------ */
|
||||
/*
|
||||
HTTrack Website Copier, Offline Browser for Windows and Unix
|
||||
Copyright (C) 2026 Xavier Roche and other contributors
|
||||
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Ethical use: we kindly ask that you NOT use this software to harvest email
|
||||
addresses or to collect any other private information about people. Doing so
|
||||
would dishonor our work and waste the many hours we have spent on it.
|
||||
|
||||
Please visit our Website: http://www.httrack.com
|
||||
*/
|
||||
|
||||
/* Fuzz the cache-index (.ndx) parser: a corrupt or truncated index must not
|
||||
walk the length-prefixed cache_brstr/cache_binput scan past the buffer.
|
||||
Mirrors the -#C cache-listing scan (htscoremain.c). */
|
||||
#include "fuzz.h"
|
||||
#include "htscache.h"
|
||||
#include "htslib.h"
|
||||
|
||||
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||
char *buf = fuzz_strdup(data, size);
|
||||
const char *const end = buf + size;
|
||||
char firstline[256];
|
||||
char *a = buf;
|
||||
|
||||
/* header: two length-prefixed fields (version, last-modified) */
|
||||
a += cache_brstr(a, firstline, sizeof(firstline));
|
||||
a += cache_brstr(a, firstline, sizeof(firstline));
|
||||
|
||||
/* body: newline-delimited host/file/position triples; the length-prefixed
|
||||
scan must stay inside the buffer */
|
||||
while (a != NULL && a < end) {
|
||||
char BIGSTK line[HTS_URLMAXSIZE * 2];
|
||||
char linepos[256];
|
||||
int pos;
|
||||
|
||||
a = strchr(a + 1, '\n');
|
||||
if (a == NULL)
|
||||
break;
|
||||
a++;
|
||||
a += cache_binput(a, end, line, HTS_URLMAXSIZE);
|
||||
a += cache_binput(a, end, line + strlen(line), HTS_URLMAXSIZE);
|
||||
a += cache_binput(a, end, linepos, 200);
|
||||
sscanf(linepos, "%d", &pos);
|
||||
(void) pos;
|
||||
}
|
||||
|
||||
freet(buf);
|
||||
return 0;
|
||||
}
|
||||
74
fuzz/fuzz-header.c
Normal file
74
fuzz/fuzz-header.c
Normal file
@@ -0,0 +1,74 @@
|
||||
/* ------------------------------------------------------------ */
|
||||
/*
|
||||
HTTrack Website Copier, Offline Browser for Windows and Unix
|
||||
Copyright (C) 2026 Xavier Roche and other contributors
|
||||
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Ethical use: we kindly ask that you NOT use this software to harvest email
|
||||
addresses or to collect any other private information about people. Doing so
|
||||
would dishonor our work and waste the many hours we have spent on it.
|
||||
|
||||
Please visit our Website: http://www.httrack.com
|
||||
*/
|
||||
|
||||
/* Fuzz the HTTP response-header parser (htslib.c): treatfirstline on the
|
||||
status line, treathead on each following header. Both consume raw bytes
|
||||
off the wire and copy fields into fixed htsblk buffers; treathead also
|
||||
mutates its line in place and drives cookie parsing. */
|
||||
#include "fuzz.h"
|
||||
#include "htslib.h"
|
||||
#include "htsbauth.h"
|
||||
|
||||
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||
char *buf = fuzz_strdup(data, size);
|
||||
htsblk r;
|
||||
t_cookie *cookie = calloct(1, sizeof(*cookie));
|
||||
char *line = malloct(size + 1);
|
||||
char *p = buf;
|
||||
int first = 1;
|
||||
|
||||
memset(&r, 0, sizeof(r));
|
||||
cookie->max_len = (int) sizeof(cookie->data);
|
||||
r.location = malloct(HTS_URLMAXSIZE * 2);
|
||||
r.location[0] = '\0';
|
||||
|
||||
/* feed one header line at a time, as the receive loop does */
|
||||
while (p != NULL && *p != '\0') {
|
||||
char *nl = strchr(p, '\n');
|
||||
size_t n = (nl != NULL) ? (size_t) (nl - p) : strlen(p);
|
||||
size_t i, len = 0;
|
||||
|
||||
/* binput drops every '\r' on the wire; mirror it */
|
||||
for (i = 0; i < n; i++)
|
||||
if (p[i] != '\r')
|
||||
line[len++] = p[i];
|
||||
line[len] = '\0';
|
||||
if (first) {
|
||||
treatfirstline(&r, line);
|
||||
first = 0;
|
||||
} else {
|
||||
treathead(cookie, "www.example.com", "/", &r, line);
|
||||
}
|
||||
p = (nl != NULL) ? nl + 1 : NULL;
|
||||
}
|
||||
|
||||
freet(r.location);
|
||||
freet(line);
|
||||
freet(cookie);
|
||||
freet(buf);
|
||||
return 0;
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
# Drive every built harness against its seed corpus.
|
||||
# run-fuzzers.sh <build-fuzz-dir> check deterministic replay (CI smoke)
|
||||
# run-fuzzers.sh <build-fuzz-dir> [seconds] timed mutation run (discovery)
|
||||
# Replay is crash/leak-only and never mutates, so it can't hit strjoker's
|
||||
# catastrophic backtracking; the timed mode can, hence the per-unit -timeout.
|
||||
# Replay is crash/leak-only and never mutates; the per-unit -timeout guards
|
||||
# both modes against pathological slowdowns (e.g. pre-#501 strjoker).
|
||||
set -euo pipefail
|
||||
|
||||
srcdir=$(cd "$(dirname "$0")" && pwd)
|
||||
|
||||
169
src/htsback.c
169
src/htsback.c
@@ -1646,138 +1646,104 @@ int back_add(struct_back * sback, httrackp * opt, cache_back * cache, const char
|
||||
}
|
||||
}
|
||||
// tester cache
|
||||
if ((strcmp(adr, "file://")) /* pas fichier */
|
||||
&&((!test) || (cache->type == 1)) /* cache prioritaire, laisser passer en test! */
|
||||
&&((strnotempty(save)) || (strcmp(fil, "/robots.txt") == 0))) { // si en test on ne doit pas utiliser le cache sinon telescopage avec le 302..
|
||||
#if HTS_FAST_CACHE
|
||||
if ((strcmp(adr, "file://")) /* pas fichier */
|
||||
&& ((!test) ||
|
||||
(cache->type == 1)) /* cache prioritaire, laisser passer en test! */
|
||||
&&
|
||||
((strnotempty(save)) || (strcmp(fil, "/robots.txt") ==
|
||||
0))) { // si en test on ne doit pas utiliser le
|
||||
// cache sinon telescopage avec le 302..
|
||||
intptr_t hash_pos;
|
||||
int hash_pos_return = 0;
|
||||
#else
|
||||
char *a = NULL;
|
||||
#endif
|
||||
#if HTS_FAST_CACHE
|
||||
|
||||
if (cache->hashtable) {
|
||||
#else
|
||||
if (cache->use) {
|
||||
#endif
|
||||
char BIGSTK buff[HTS_URLMAXSIZE * 4];
|
||||
|
||||
#if HTS_FAST_CACHE
|
||||
strcpybuff(buff, adr);
|
||||
strcatbuff(buff, fil);
|
||||
hash_pos_return = coucal_read(cache->hashtable, buff, &hash_pos);
|
||||
#else
|
||||
buff[0] = '\0';
|
||||
strcatbuff(buff, "\n");
|
||||
strcatbuff(buff, adr);
|
||||
strcatbuff(buff, "\n");
|
||||
strcatbuff(buff, fil);
|
||||
strcatbuff(buff, "\n");
|
||||
a = strstr(cache->use, buff);
|
||||
#endif
|
||||
|
||||
// Ok, noté en cache->. mais bien présent dans le cache ou sur disque?
|
||||
#if HTS_FAST_CACHE
|
||||
// negative values when data is not in cache
|
||||
if (hash_pos_return < 0) {
|
||||
#else
|
||||
if (a) {
|
||||
#endif
|
||||
if (!test) { // non mode test
|
||||
#if HTS_FAST_CACHE==0
|
||||
int pos = -1;
|
||||
if (!test) { // not test mode
|
||||
/* note: no check with IS_DELAYED_EXT() enabled - postcheck by
|
||||
* client please! */
|
||||
if (save[0] != '\0' && !IS_DELAYED_EXT(save) &&
|
||||
fsize_utf8(fconv(catbuff, sizeof(catbuff), save)) <=
|
||||
0) { // final file missing or empty
|
||||
int found = 0;
|
||||
|
||||
a += strlen(buff);
|
||||
sscanf(a, "%d", &pos); // lire position
|
||||
#endif
|
||||
/* It is possible that the file has been moved due to changes in
|
||||
* build structure */
|
||||
{
|
||||
char BIGSTK previous_save[HTS_URLMAXSIZE * 2];
|
||||
htsblk r;
|
||||
|
||||
#if HTS_FAST_CACHE==0
|
||||
if (pos < 0) { // pas de mise en cache data, vérifier existence
|
||||
#endif
|
||||
/* note: no check with IS_DELAYED_EXT() enabled - postcheck by client please! */
|
||||
if (save[0] != '\0' && !IS_DELAYED_EXT(save) && fsize_utf8(fconv(catbuff, sizeof(catbuff), save)) <= 0) { // fichier final n'existe pas ou est vide!
|
||||
int found = 0;
|
||||
|
||||
/* It is possible that the file has been moved due to changes in build structure */
|
||||
{
|
||||
char BIGSTK previous_save[HTS_URLMAXSIZE * 2];
|
||||
htsblk r;
|
||||
|
||||
previous_save[0] = '\0';
|
||||
r =
|
||||
cache_readex(opt, cache, adr, fil, /*head */ NULL,
|
||||
previous_save[0] = '\0';
|
||||
r = cache_readex(opt, cache, adr, fil, /*head */ NULL,
|
||||
/*bound to back[p] (temporary) */
|
||||
back[p].location_buffer, previous_save, /*ro */
|
||||
1);
|
||||
/* Is supposed to be on disk only */
|
||||
if (r.is_write && previous_save[0] != '\0') {
|
||||
/* Exists, but with another (old) filename: rename (almost) silently */
|
||||
if (strcmp(previous_save, save) != 0
|
||||
&& fexist_utf8(fconv(catbuff, sizeof(catbuff), previous_save))) {
|
||||
rename(fconv(catbuff, sizeof(catbuff), previous_save),
|
||||
fconv(catbuff2, sizeof(catbuff2), save));
|
||||
if (fexist_utf8(fconv(catbuff, sizeof(catbuff), save))) {
|
||||
found = 1;
|
||||
hts_log_print(opt, LOG_DEBUG,
|
||||
"File '%s' has been renamed since last mirror to '%s' ; applying changes",
|
||||
previous_save, save);
|
||||
} else {
|
||||
hts_log_print(opt, LOG_ERROR,
|
||||
"Could not rename '%s' to '%s' ; will have to retransfer it",
|
||||
previous_save, save);
|
||||
}
|
||||
/* Is supposed to be on disk only */
|
||||
if (r.is_write && previous_save[0] != '\0') {
|
||||
/* Exists, but with another (old) filename: rename (almost)
|
||||
* silently */
|
||||
if (strcmp(previous_save, save) != 0 &&
|
||||
fexist_utf8(
|
||||
fconv(catbuff, sizeof(catbuff), previous_save))) {
|
||||
rename(fconv(catbuff, sizeof(catbuff), previous_save),
|
||||
fconv(catbuff2, sizeof(catbuff2), save));
|
||||
if (fexist_utf8(fconv(catbuff, sizeof(catbuff), save))) {
|
||||
found = 1;
|
||||
hts_log_print(opt, LOG_DEBUG,
|
||||
"File '%s' has been renamed since last "
|
||||
"mirror to '%s' ; applying changes",
|
||||
previous_save, save);
|
||||
} else {
|
||||
hts_log_print(opt, LOG_ERROR,
|
||||
"Could not rename '%s' to '%s' ; will have "
|
||||
"to retransfer it",
|
||||
previous_save, save);
|
||||
}
|
||||
}
|
||||
back[p].location_buffer[0] = '\0';
|
||||
}
|
||||
back[p].location_buffer[0] = '\0';
|
||||
}
|
||||
|
||||
/* Not found ? */
|
||||
if (!found) {
|
||||
#if HTS_FAST_CACHE
|
||||
hash_pos_return = 0;
|
||||
#else
|
||||
a = NULL;
|
||||
#endif
|
||||
// dévalider car non présent sur disque dans structure originale!!!
|
||||
// sinon, le fichier est ok à priori, mais on renverra un if-modified-since pour
|
||||
// en être sûr
|
||||
if (opt->norecatch) { // tester norecatch
|
||||
if (!fexist_utf8(fconv(catbuff, sizeof(catbuff), save))) { // fichier existe pas mais déclaré: on l'a effacé
|
||||
FILE *fp = FOPEN(fconv(catbuff, sizeof(catbuff), save), "wb");
|
||||
/* Not found ? */
|
||||
if (!found) {
|
||||
// invalidate: gone from disk, force a refetch
|
||||
hash_pos_return = 0;
|
||||
if (opt->norecatch) {
|
||||
if (!fexist_utf8(fconv(
|
||||
catbuff, sizeof(catbuff),
|
||||
save))) { // declared but missing: user erased it
|
||||
FILE *fp =
|
||||
FOPEN(fconv(catbuff, sizeof(catbuff), save), "wb");
|
||||
|
||||
if (fp)
|
||||
fclose(fp);
|
||||
hts_log_print(opt, LOG_WARNING,
|
||||
"Previous file '%s' not found (erased by user ?), ignoring: %s%s",
|
||||
save, back[p].url_adr, back[p].url_fil);
|
||||
}
|
||||
} else {
|
||||
if (fp)
|
||||
fclose(fp);
|
||||
hts_log_print(opt, LOG_WARNING,
|
||||
"Previous file '%s' not found (erased by user ?), recatching: %s%s",
|
||||
"Previous file '%s' not found (erased by "
|
||||
"user ?), ignoring: %s%s",
|
||||
save, back[p].url_adr, back[p].url_fil);
|
||||
}
|
||||
} else {
|
||||
hts_log_print(opt, LOG_WARNING,
|
||||
"Previous file '%s' not found (erased by user "
|
||||
"?), recatching: %s%s",
|
||||
save, back[p].url_adr, back[p].url_fil);
|
||||
}
|
||||
} // fsize() <= 0
|
||||
#if HTS_FAST_CACHE==0
|
||||
}
|
||||
#endif
|
||||
}
|
||||
} // fsize() <= 0
|
||||
}
|
||||
}
|
||||
//
|
||||
} else {
|
||||
#if HTS_FAST_CACHE
|
||||
hash_pos_return = 0;
|
||||
#else
|
||||
a = NULL;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Existe pas en cache, ou bien pas de cache présent
|
||||
#if HTS_FAST_CACHE
|
||||
if (hash_pos_return) { // OK existe en cache (et données aussi)!
|
||||
#else
|
||||
if (a != NULL) { // OK existe en cache (et données aussi)!
|
||||
#endif
|
||||
if (hash_pos_return) { // in cache, with data
|
||||
const int cache_is_prioritary = cache->type == 1
|
||||
|| opt->state.stop != 0;
|
||||
if (cache_is_prioritary) { // cache prioritaire (pas de test if-modified..)
|
||||
@@ -1898,7 +1864,8 @@ int back_add(struct_back * sback, httrackp * opt, cache_back * cache, const char
|
||||
#endif
|
||||
}
|
||||
}
|
||||
/* Not in cache ; maybe in temporary cache ? Warning: non-movable "url_sav" */
|
||||
/* Not in cache ; maybe in temporary cache ? Warning: non-movable
|
||||
"url_sav" */
|
||||
else if (back_unserialize_ref(opt, adr, fil, &itemback) == 0) {
|
||||
const off_t file_size = fsize_utf8(itemback->url_sav);
|
||||
|
||||
|
||||
@@ -307,14 +307,25 @@ int cookie_load(t_cookie * cookie, const char *fpath, const char *name) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// écrire cookies.txt
|
||||
// !=0 : erreur
|
||||
/* Write cookies.txt; returns 0 on success. The jar holds live session
|
||||
cookies, so keep it owner-only on Unix (Windows inherits folder ACLs). */
|
||||
int cookie_save(t_cookie * cookie, const char *name) {
|
||||
char catbuff[CATBUFF_SIZE];
|
||||
|
||||
if (strnotempty(cookie->data)) {
|
||||
char BIGSTK line[8192];
|
||||
#ifdef _WIN32
|
||||
FILE *fp = fopen(fconv(catbuff, sizeof(catbuff), name), "wb");
|
||||
#else
|
||||
const int fd = open(fconv(catbuff, sizeof(catbuff), name),
|
||||
O_WRONLY | O_CREAT | O_TRUNC, HTS_PROTECT_FILE);
|
||||
FILE *fp = fd != -1 ? fdopen(fd, "wb") : NULL;
|
||||
|
||||
if (fd != -1 && fp == NULL)
|
||||
close(fd); /* fdopen failed: don't leak the descriptor */
|
||||
if (fp != NULL) /* O_CREAT's mode skips pre-existing jars: tighten those */
|
||||
(void) fchmod(fd, HTS_PROTECT_FILE);
|
||||
#endif
|
||||
|
||||
if (fp) {
|
||||
char *a = cookie->data;
|
||||
|
||||
980
src/htscache.c
980
src/htscache.c
File diff suppressed because it is too large
Load Diff
@@ -89,14 +89,11 @@ typedef enum {
|
||||
the involved files are absent. */
|
||||
void hts_cache_reconcile(httrackp *opt, hts_cache_reconcile_mode mode);
|
||||
|
||||
int cache_writedata(FILE * cache_ndx, FILE * cache_dat, const char *str1,
|
||||
const char *str2, char *outbuff, int len);
|
||||
int cache_readdata(cache_back * cache, const char *str1, const char *str2,
|
||||
char **inbuff, int *len);
|
||||
|
||||
void cache_rstr(FILE *fp, char *s, size_t s_size);
|
||||
char *cache_rstr_addr(FILE * fp);
|
||||
int cache_brstr(char *adr, char *s, size_t s_size);
|
||||
/* binput over a NUL-terminated buffer, bounded: no read starts at/past end. */
|
||||
int cache_binput(char *adr, const char *end, char *s, int max);
|
||||
int cache_brint(char *adr, int *i);
|
||||
void cache_rint(FILE * fp, int *i);
|
||||
void cache_rLLint(FILE * fp, LLint * i);
|
||||
|
||||
@@ -78,14 +78,6 @@ static void selftest_open_for_read(cache_back *cache, httrackp *opt) {
|
||||
}
|
||||
|
||||
static void selftest_close(cache_back *cache) {
|
||||
if (cache->dat != NULL) {
|
||||
fclose(cache->dat);
|
||||
cache->dat = NULL;
|
||||
}
|
||||
if (cache->ndx != NULL) {
|
||||
fclose(cache->ndx);
|
||||
cache->ndx = NULL;
|
||||
}
|
||||
if (cache->zipOutput != NULL) {
|
||||
zipClose(cache->zipOutput,
|
||||
"Created by HTTrack Website Copier (cache self-test)");
|
||||
@@ -989,26 +981,15 @@ int cache_reconcile_selftest(httrackp *opt, const char *dir) {
|
||||
failures +=
|
||||
reconcile_expect(opt, "hts-cache/old.zip", SOLID, "promote-zip-noop");
|
||||
|
||||
/* PROMOTE: a pure-legacy old generation is promoted too (was dead when no
|
||||
zip cache existed) */
|
||||
/* PROMOTE: the removed pre-3.31 legacy pair is left alone */
|
||||
reconcile_wipe(opt);
|
||||
reconcile_put(opt, "hts-cache/old.dat", SOLID);
|
||||
reconcile_put(opt, "hts-cache/old.ndx", TINY);
|
||||
hts_cache_reconcile(opt, CACHE_RECONCILE_PROMOTE);
|
||||
failures += reconcile_expect(opt, "hts-cache/new.dat", SOLID, "promote-dat");
|
||||
failures += reconcile_expect(opt, "hts-cache/new.ndx", TINY, "promote-dat");
|
||||
failures += reconcile_expect(opt, "hts-cache/old.dat", -1, "promote-dat");
|
||||
|
||||
/* PROMOTE: a half-written legacy new pair is replaced by the old pair */
|
||||
reconcile_wipe(opt);
|
||||
reconcile_put(opt, "hts-cache/new.dat", TINY);
|
||||
reconcile_put(opt, "hts-cache/old.dat", SOLID);
|
||||
reconcile_put(opt, "hts-cache/old.ndx", TINY);
|
||||
hts_cache_reconcile(opt, CACHE_RECONCILE_PROMOTE);
|
||||
failures +=
|
||||
reconcile_expect(opt, "hts-cache/new.dat", SOLID, "promote-dat-partial");
|
||||
failures +=
|
||||
reconcile_expect(opt, "hts-cache/new.ndx", TINY, "promote-dat-partial");
|
||||
failures += reconcile_expect(opt, "hts-cache/new.dat", -1, "promote-dat");
|
||||
failures += reconcile_expect(opt, "hts-cache/new.ndx", -1, "promote-dat");
|
||||
failures += reconcile_expect(opt, "hts-cache/old.dat", SOLID, "promote-dat");
|
||||
failures += reconcile_expect(opt, "hts-cache/old.ndx", TINY, "promote-dat");
|
||||
|
||||
/* INTERRUPTED: no lock file, no action */
|
||||
reconcile_wipe(opt);
|
||||
@@ -1058,7 +1039,7 @@ int cache_reconcile_selftest(httrackp *opt, const char *dir) {
|
||||
failures +=
|
||||
reconcile_expect(opt, "hts-cache/new.zip", MID, "interrupted-bignew");
|
||||
|
||||
/* INTERRUPTED: the legacy pair follows the same size rule (was dead code) */
|
||||
/* INTERRUPTED: the removed pre-3.31 legacy pair is left alone */
|
||||
reconcile_wipe(opt);
|
||||
reconcile_put(opt, "hts-in_progress.lock", 0);
|
||||
reconcile_put(opt, "hts-cache/new.dat", TINY);
|
||||
@@ -1067,9 +1048,13 @@ int cache_reconcile_selftest(httrackp *opt, const char *dir) {
|
||||
reconcile_put(opt, "hts-cache/old.ndx", MID);
|
||||
hts_cache_reconcile(opt, CACHE_RECONCILE_INTERRUPTED);
|
||||
failures +=
|
||||
reconcile_expect(opt, "hts-cache/new.dat", SOLID, "interrupted-dat");
|
||||
reconcile_expect(opt, "hts-cache/new.dat", TINY, "interrupted-dat");
|
||||
failures +=
|
||||
reconcile_expect(opt, "hts-cache/new.ndx", MID, "interrupted-dat");
|
||||
reconcile_expect(opt, "hts-cache/new.ndx", TINY, "interrupted-dat");
|
||||
failures +=
|
||||
reconcile_expect(opt, "hts-cache/old.dat", SOLID, "interrupted-dat");
|
||||
failures +=
|
||||
reconcile_expect(opt, "hts-cache/old.ndx", MID, "interrupted-dat");
|
||||
|
||||
/* ROLLBACK: the old zip generation is restored (a zip cache used to lose
|
||||
its only good generation here) */
|
||||
@@ -1089,17 +1074,18 @@ int cache_reconcile_selftest(httrackp *opt, const char *dir) {
|
||||
failures += reconcile_expect(opt, "hts-cache/new.lst", MID, "rollback-lst");
|
||||
failures += reconcile_expect(opt, "hts-cache/new.txt", MID, "rollback-txt");
|
||||
|
||||
/* ROLLBACK: full legacy generation incl. sidecars (historical behavior) */
|
||||
/* ROLLBACK: sidecars restored, the removed pre-3.31 pair left alone */
|
||||
reconcile_wipe(opt);
|
||||
reconcile_put(opt, "hts-cache/new.dat", TINY);
|
||||
reconcile_put(opt, "hts-cache/new.ndx", TINY);
|
||||
reconcile_put(opt, "hts-cache/old.dat", SOLID);
|
||||
reconcile_put(opt, "hts-cache/old.ndx", MID);
|
||||
reconcile_put(opt, "hts-cache/old.lst", MID);
|
||||
reconcile_put(opt, "hts-cache/old.txt", MID);
|
||||
hts_cache_reconcile(opt, CACHE_RECONCILE_ROLLBACK);
|
||||
failures += reconcile_expect(opt, "hts-cache/new.dat", SOLID, "rollback-dat");
|
||||
failures += reconcile_expect(opt, "hts-cache/new.ndx", MID, "rollback-dat");
|
||||
failures += reconcile_expect(opt, "hts-cache/new.dat", TINY, "rollback-dat");
|
||||
failures += reconcile_expect(opt, "hts-cache/new.ndx", -1, "rollback-dat");
|
||||
failures += reconcile_expect(opt, "hts-cache/old.dat", SOLID, "rollback-dat");
|
||||
failures += reconcile_expect(opt, "hts-cache/old.ndx", MID, "rollback-dat");
|
||||
failures += reconcile_expect(opt, "hts-cache/new.lst", MID, "rollback-dat");
|
||||
failures += reconcile_expect(opt, "hts-cache/new.txt", MID, "rollback-dat");
|
||||
|
||||
@@ -1113,6 +1099,101 @@ int cache_reconcile_selftest(httrackp *opt, const char *dir) {
|
||||
return failures;
|
||||
}
|
||||
|
||||
/* The pre-3.31 .dat/.ndx import was removed: cache_init must refuse the
|
||||
legacy pair, leave the files in place, and not flag an update run. */
|
||||
int cache_legacy_refused_selftest(httrackp *opt, const char *dir) {
|
||||
int failures = 0;
|
||||
int variant;
|
||||
|
||||
selftest_tag = "cache-legacy";
|
||||
golden_setup(opt, dir);
|
||||
#ifdef _WIN32
|
||||
mkdir(reconcile_st_path(opt, "hts-cache"));
|
||||
#else
|
||||
mkdir(reconcile_st_path(opt, "hts-cache"), HTS_PROTECT_FOLDER);
|
||||
#endif
|
||||
|
||||
/* variant 0: old.dat/old.ndx (--update layout); 1: new.dat/new.ndx (ro) */
|
||||
for (variant = 0; variant < 2; variant++) {
|
||||
cache_back cache;
|
||||
const char *const dat =
|
||||
variant == 0 ? "hts-cache/old.dat" : "hts-cache/new.dat";
|
||||
const char *const ndx =
|
||||
variant == 0 ? "hts-cache/old.ndx" : "hts-cache/new.ndx";
|
||||
|
||||
reconcile_wipe(opt);
|
||||
reconcile_put(opt, dat, 4096);
|
||||
reconcile_put(opt, ndx, 4096);
|
||||
opt->is_update = 0;
|
||||
selftest_open(&cache, opt, /*ro*/ variant == 1);
|
||||
if (cache_readable(&cache)) {
|
||||
printf("cache-legacy: variant %d imported a removed format\n", variant);
|
||||
failures++;
|
||||
}
|
||||
if (coucal_nitems(cache.hashtable) != 0) { /* no phantom index entries */
|
||||
printf("cache-legacy: variant %d imported index entries\n", variant);
|
||||
failures++;
|
||||
}
|
||||
if (opt->is_update) {
|
||||
printf("cache-legacy: variant %d flagged an update\n", variant);
|
||||
failures++;
|
||||
}
|
||||
if (fsize(reconcile_st_path(opt, dat)) != 4096 ||
|
||||
fsize(reconcile_st_path(opt, ndx)) != 4096) {
|
||||
printf("cache-legacy: variant %d touched the legacy files\n", variant);
|
||||
failures++;
|
||||
}
|
||||
if (cache.lst != NULL) {
|
||||
fclose(cache.lst);
|
||||
cache.lst = NULL;
|
||||
}
|
||||
if (cache.txt != NULL) {
|
||||
fclose(cache.txt);
|
||||
cache.txt = NULL;
|
||||
}
|
||||
selftest_close(&cache);
|
||||
}
|
||||
|
||||
/* a healthy zip must win over stray legacy files, with no refusal */
|
||||
{
|
||||
cache_back cache;
|
||||
const char *const body = "<html>zip wins</html>";
|
||||
|
||||
reconcile_wipe(opt);
|
||||
selftest_open_for_write(&cache, opt);
|
||||
store_entry(opt, &cache, "example.com", "/", "example.com/index.html", 200,
|
||||
"OK", "text/html", "utf-8", "", "", "", "", body, strlen(body));
|
||||
selftest_close(&cache);
|
||||
reconcile_put(opt, "hts-cache/old.ndx", 4096);
|
||||
reconcile_put(opt, "hts-cache/old.dat", 4096);
|
||||
selftest_open_for_read(&cache, opt);
|
||||
if (!cache_readable(&cache)) {
|
||||
printf("cache-legacy: stray legacy files shadowed the zip cache\n");
|
||||
failures++;
|
||||
}
|
||||
failures +=
|
||||
check_entry(opt, &cache, "example.com", "/", 200, "OK", "text/html",
|
||||
"utf-8", "", "", "", "", body, strlen(body));
|
||||
if (fsize(reconcile_st_path(opt, "hts-cache/old.ndx")) != 4096 ||
|
||||
fsize(reconcile_st_path(opt, "hts-cache/old.dat")) != 4096) {
|
||||
printf("cache-legacy: zip-wins pass touched the legacy files\n");
|
||||
failures++;
|
||||
}
|
||||
if (cache.lst != NULL) {
|
||||
fclose(cache.lst);
|
||||
cache.lst = NULL;
|
||||
}
|
||||
if (cache.txt != NULL) {
|
||||
fclose(cache.txt);
|
||||
cache.txt = NULL;
|
||||
}
|
||||
selftest_close(&cache);
|
||||
}
|
||||
|
||||
reconcile_wipe(opt);
|
||||
return failures;
|
||||
}
|
||||
|
||||
/* --- read-side corruption injection --------------------------------------- */
|
||||
|
||||
/* canary read back intact after each corruption; victim gets the byte surgery
|
||||
|
||||
@@ -60,6 +60,10 @@ int cache_write_failure_selftest(httrackp *opt, const char *dir);
|
||||
under <dir>. Returns the failed-check count. */
|
||||
int cache_reconcile_selftest(httrackp *opt, const char *dir);
|
||||
|
||||
/* Verify cache_init refuses a pre-3.31 .dat/.ndx cache without touching it.
|
||||
Returns the number of failed checks (0 = pass). */
|
||||
int cache_legacy_refused_selftest(httrackp *opt, const char *dir);
|
||||
|
||||
/* Inject read-side corruption (zip byte surgery: bad size, header, deflate)
|
||||
under <dir> and assert every case degrades to STATUSCODE_INVALID without
|
||||
tainting a sibling entry. */
|
||||
|
||||
218
src/htscharset.c
218
src/htscharset.c
@@ -84,7 +84,7 @@ static int hts_equalsAlphanum(const char *a, const char *b) {
|
||||
|
||||
/* Copy the memory region [s .. s + size - 1 ] as a \0-terminated string. */
|
||||
static char *hts_stringMemCopy(const char *s, size_t size) {
|
||||
char *dest = malloc(size + 1);
|
||||
char *dest = malloct(size + 1);
|
||||
|
||||
if (dest != NULL) {
|
||||
memcpy(dest, s, size);
|
||||
@@ -549,42 +549,6 @@ char *hts_convertStringFromUTF8(const char *s, size_t size, const char *charset)
|
||||
|
||||
#endif
|
||||
|
||||
HTS_STATIC char *hts_getCharsetFromContentType(const char *mime) {
|
||||
/* text/html; charset=utf-8 */
|
||||
const char *const charset = "charset";
|
||||
char *pos = strstr(mime, charset);
|
||||
|
||||
if (pos != NULL) {
|
||||
/* Skip spaces */
|
||||
int eq = 0;
|
||||
|
||||
for(pos += strlen(charset);
|
||||
*pos == ' ' || *pos == '=' || *pos == '"' || *pos == '\''; pos++) {
|
||||
if (*pos == '=') {
|
||||
eq = 1;
|
||||
}
|
||||
}
|
||||
if (eq == 1) {
|
||||
int len;
|
||||
|
||||
for(len = 0;
|
||||
pos[len] == ' ' || pos[len] == ';' || pos[len] == '"' || *pos == '\'';
|
||||
pos++) ;
|
||||
if (len != 0) {
|
||||
char *const s = malloc(len + 1);
|
||||
int i;
|
||||
|
||||
for(i = 0; i < len; i++) {
|
||||
s[i] = pos[i];
|
||||
}
|
||||
s[len] = '\0';
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
#define strcasecmp(a,b) stricmp(a,b)
|
||||
#define strncasecmp(a,b,n) strnicmp(a,b,n)
|
||||
@@ -644,58 +608,106 @@ int hts_isCharsetUTF8(const char *charset) {
|
||||
|| strcasecmp(charset, "utf8") == 0 );
|
||||
}
|
||||
|
||||
/* Extract X from a "text/html; charset=X" content= attribute value. */
|
||||
static char *charset_from_content(const char *s, size_t len) {
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i + 7 < len; i++) {
|
||||
if ((i == 0 || is_space(s[i - 1]) || s[i - 1] == ';') &&
|
||||
strncasecmp(&s[i], "charset", 7) == 0 && is_space_or_equal(s[i + 7])) {
|
||||
size_t j, val;
|
||||
|
||||
for (j = i + 7; j < len && is_space_or_equal_or_quote(s[j]); j++)
|
||||
;
|
||||
for (val = j; j < len && s[j] != '"' && s[j] != '\'' && s[j] != ';' &&
|
||||
!is_space(s[j]);
|
||||
j++)
|
||||
;
|
||||
if (j != val) {
|
||||
return hts_stringMemCopy(&s[val], j - val);
|
||||
}
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char *hts_getCharsetFromMeta(const char *html, size_t size) {
|
||||
int i;
|
||||
size_t i;
|
||||
|
||||
/* <META HTTP-EQUIV="CONTENT-TYPE" CONTENT="text/html; charset=utf-8" > */
|
||||
for(i = 0; i < size; i++) {
|
||||
if (html[i] == '<' && strncasecmp(&html[i + 1], "meta", 4) == 0
|
||||
&& is_space(html[i + 5])) {
|
||||
/* Skip spaces */
|
||||
for(i += 5; is_space(html[i]); i++) ;
|
||||
if (strncasecmp(&html[i], "HTTP-EQUIV", 10) == 0
|
||||
&& is_space_or_equal(html[i + 10])) {
|
||||
for(i += 10; is_space_or_equal_or_quote(html[i]); i++) ;
|
||||
if (strncasecmp(&html[i], "CONTENT-TYPE", 12) == 0) {
|
||||
for(i += 12; is_space_or_equal_or_quote(html[i]); i++) ;
|
||||
if (strncasecmp(&html[i], "CONTENT", 7) == 0
|
||||
&& is_space_or_equal(html[i + 7])) {
|
||||
for(i += 7; is_space_or_equal_or_quote(html[i]); i++) ;
|
||||
/* Skip content-type */
|
||||
for(;
|
||||
i < size && html[i] != ';' && html[i] != '"' && html[i] != '\'';
|
||||
i++) ;
|
||||
/* Expect charset attribute here */
|
||||
if (html[i] == ';') {
|
||||
for(i++; is_space(html[i]); i++) ;
|
||||
/* Look for charset */
|
||||
if (strncasecmp(&html[i], "charset", 7) == 0
|
||||
&& is_space_or_equal(html[i + 7])) {
|
||||
int len;
|
||||
/* HTML5 <meta charset=X> or legacy
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=X">,
|
||||
attributes in any order; first resolvable tag wins. */
|
||||
for (i = 0; i + 6 < size; i++) {
|
||||
size_t j;
|
||||
const char *equiv = NULL, *content = NULL;
|
||||
size_t equiv_len = 0, content_len = 0;
|
||||
|
||||
for(i += 7; is_space_or_equal(html[i]) || html[i] == '\'';
|
||||
i++) ;
|
||||
/* Charset */
|
||||
for(len = 0;
|
||||
i + len < size && html[i + len] != '"'
|
||||
&& html[i + len] != '\'' && html[i + len] != ' '; len++) ;
|
||||
/* No error ? */
|
||||
if (len != 0 && i < size) {
|
||||
char *const s = malloc(len + 1);
|
||||
int j;
|
||||
if (html[i] != '<' || strncasecmp(&html[i + 1], "meta", 4) != 0 ||
|
||||
!is_space(html[i + 5])) {
|
||||
continue;
|
||||
}
|
||||
/* Attribute scan, strictly bounded by size. */
|
||||
for (j = i + 6; j < size && html[j] != '>';) {
|
||||
size_t name, name_len, val = 0, val_len = 0;
|
||||
|
||||
for(j = 0; j < len; j++) {
|
||||
s[j] = html[i + j];
|
||||
}
|
||||
s[len] = '\0';
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (is_space(html[j]) || html[j] == '/') {
|
||||
j++;
|
||||
continue;
|
||||
}
|
||||
for (name = j; j < size && html[j] != '=' && html[j] != '>' &&
|
||||
html[j] != '/' && !is_space(html[j]);
|
||||
j++)
|
||||
;
|
||||
name_len = j - name;
|
||||
for (; j < size && is_space(html[j]); j++)
|
||||
;
|
||||
if (j < size && html[j] == '=') {
|
||||
for (j++; j < size && is_space(html[j]); j++)
|
||||
;
|
||||
if (j < size && (html[j] == '"' || html[j] == '\'')) {
|
||||
const char quote = html[j++];
|
||||
|
||||
for (val = j; j < size && html[j] != quote; j++)
|
||||
;
|
||||
if (j >= size) /* unterminated quote: drop the tag */
|
||||
break;
|
||||
val_len = j++ - val;
|
||||
} else {
|
||||
/* unquoted: ends at whitespace or '>' only (HTML5 prescan); '/'
|
||||
belongs to the value except as the tail of a self-close */
|
||||
for (val = j; j < size && !is_space(html[j]) && html[j] != '>'; j++)
|
||||
;
|
||||
val_len = j - val;
|
||||
if (val_len != 0 && j < size && html[j] == '>' &&
|
||||
html[val + val_len - 1] == '/') {
|
||||
val_len--;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* first occurrence of each attribute wins, as in browsers */
|
||||
if (val_len != 0) {
|
||||
if (name_len == 7 && strncasecmp(&html[name], "charset", 7) == 0) {
|
||||
return hts_stringMemCopy(&html[val], val_len);
|
||||
} else if (equiv == NULL && name_len == 10 &&
|
||||
strncasecmp(&html[name], "http-equiv", 10) == 0) {
|
||||
equiv = &html[val];
|
||||
equiv_len = val_len;
|
||||
} else if (content == NULL && name_len == 7 &&
|
||||
strncasecmp(&html[name], "content", 7) == 0) {
|
||||
content = &html[val];
|
||||
content_len = val_len;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (equiv_len == 12 && strncasecmp(equiv, "content-type", 12) == 0 &&
|
||||
content != NULL) {
|
||||
char *const s = charset_from_content(content, content_len);
|
||||
|
||||
if (s != NULL) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
i = j;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
@@ -1140,17 +1152,45 @@ int hts_isStringUTF8(const char *s, size_t size) {
|
||||
const unsigned char *const data = (const unsigned char*) s;
|
||||
size_t i;
|
||||
|
||||
/* Strict RFC 3629: reject overlongs, surrogates, > U+10FFFF, 5/6-byte. */
|
||||
for(i = 0 ; i < size ; ) {
|
||||
/* Reader: can read bytes up to j */
|
||||
#define RD ( i < size ? data[i++] : -1 )
|
||||
const unsigned char c = data[i];
|
||||
size_t len, k;
|
||||
hts_UCS4 uc, min;
|
||||
|
||||
/* Writer: a malformed sequence means the string is not UTF-8 (return 0) */
|
||||
#define WR(C) if ((C) == -1) { return 0; }
|
||||
if (c < 0x80) {
|
||||
i++;
|
||||
continue;
|
||||
} else if (c >= 0xc2 && c <= 0xdf) {
|
||||
len = 2;
|
||||
min = 0x80;
|
||||
uc = c & 0x1f;
|
||||
} else if (c >= 0xe0 && c <= 0xef) {
|
||||
len = 3;
|
||||
min = 0x800;
|
||||
uc = c & 0x0f;
|
||||
} else if (c >= 0xf0 && c <= 0xf4) {
|
||||
len = 4;
|
||||
min = 0x10000;
|
||||
uc = c & 0x07;
|
||||
} else { /* continuation byte, overlong C0/C1, or F5..FF lead */
|
||||
return 0;
|
||||
}
|
||||
if (size - i < len) {
|
||||
return 0;
|
||||
}
|
||||
for (k = 1; k < len; k++) {
|
||||
const unsigned char cc = data[i + k];
|
||||
|
||||
/* Read Unicode character. */
|
||||
READ_UNICODE(RD, WR);
|
||||
#undef RD
|
||||
#undef WR
|
||||
if ((cc & 0xc0) != 0x80) {
|
||||
return 0;
|
||||
}
|
||||
uc = (uc << 6) | (cc & 0x3f);
|
||||
}
|
||||
if (uc < min || uc > 0x10FFFF || (uc >= 0xD800 && uc <= 0xDFFF)) {
|
||||
return 0;
|
||||
}
|
||||
i += len;
|
||||
}
|
||||
|
||||
return 1;
|
||||
|
||||
@@ -76,7 +76,8 @@ extern char *hts_convertStringIDNAToUTF8(const char *s, size_t size);
|
||||
extern int hts_isStringIDNA(const char *s, size_t size);
|
||||
|
||||
/**
|
||||
* Extract the charset from the HTML buffer "html"
|
||||
* Extract the <meta> charset from the HTML buffer "html" (HTML5 charset= or
|
||||
* legacy http-equiv form). Returns a malloc'ed string, or NULL if none.
|
||||
**/
|
||||
extern char *hts_getCharsetFromMeta(const char *html, size_t size);
|
||||
|
||||
@@ -86,7 +87,8 @@ extern char *hts_getCharsetFromMeta(const char *html, size_t size);
|
||||
extern int hts_isStringAscii(const char *s, size_t size);
|
||||
|
||||
/**
|
||||
* Is the given string an UTF-8 string ?
|
||||
* Is the given string valid UTF-8 ? Strict RFC 3629: overlong forms,
|
||||
* surrogates, codepoints above U+10FFFF and 5/6-byte sequences are rejected.
|
||||
**/
|
||||
extern int hts_isStringUTF8(const char *s, size_t size);
|
||||
|
||||
|
||||
@@ -76,9 +76,6 @@ Please visit our Website: http://www.httrack.com
|
||||
// always direct-to-disk (0/1)
|
||||
#define HTS_DIRECTDISK_ALWAYS 1
|
||||
|
||||
// fast cache (build hash table)
|
||||
#define HTS_FAST_CACHE 1
|
||||
|
||||
// le > peut être considéré comme un tag de fermeture de commentaire (<!-- > est
|
||||
// valide)
|
||||
#define GT_ENDS_COMMENT 1
|
||||
|
||||
@@ -134,18 +134,6 @@ RUN_CALLBACK0(opt, end); \
|
||||
back_delete_all(opt, &cache, sback); \
|
||||
back_free(&sback); \
|
||||
checkrobots_free(&robots); \
|
||||
if (cache.use) { \
|
||||
freet(cache.use); \
|
||||
cache.use = NULL; \
|
||||
} \
|
||||
if (cache.dat) { \
|
||||
fclose(cache.dat); \
|
||||
cache.dat = NULL; \
|
||||
} \
|
||||
if (cache.ndx) { \
|
||||
fclose(cache.ndx); \
|
||||
cache.ndx = NULL; \
|
||||
} \
|
||||
if (cache.zipOutput) { \
|
||||
zipClose(cache.zipOutput, \
|
||||
"Created by HTTrack Website Copier/" HTTRACK_VERSION); \
|
||||
@@ -155,10 +143,6 @@ RUN_CALLBACK0(opt, end); \
|
||||
unzClose(cache.zipInput); \
|
||||
cache.zipInput = NULL; \
|
||||
} \
|
||||
if (cache.olddat) { \
|
||||
fclose(cache.olddat); \
|
||||
cache.olddat = NULL; \
|
||||
} \
|
||||
if (cache.lst) { \
|
||||
fclose(cache.lst); \
|
||||
cache.lst = opt->state.strc.lst = NULL; \
|
||||
@@ -488,32 +472,11 @@ void hts_finish_makeindex(httrackp *opt, int *makeindex_done,
|
||||
*makeindex_done = 1;
|
||||
}
|
||||
|
||||
/* Flush the parsed HTML output buffer to disk, skipping the rewrite when the
|
||||
* on-disk MD5 is unchanged. */
|
||||
/* Flush the parsed HTML output buffer to disk. */
|
||||
void hts_finish_html_file(httrackp *opt, cache_back *cache, htsblk *r,
|
||||
FILE **fp, const char *ht_buff, size_t ht_len,
|
||||
const char *adr, const char *fil, const char *save) {
|
||||
char digest[32 + 2];
|
||||
off_t fsize_old =
|
||||
fsize(fconv(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt), save));
|
||||
int ok = 0;
|
||||
|
||||
digest[0] = '\0';
|
||||
domd5mem(ht_buff, ht_len, digest, 1);
|
||||
if (fsize_old == (off_t) ht_len) {
|
||||
int mlen = 0;
|
||||
char *mbuff;
|
||||
|
||||
cache_readdata(cache, "//[HTML-MD5]//", save, &mbuff, &mlen);
|
||||
if (mlen)
|
||||
mbuff[mlen] = '\0';
|
||||
if ((mlen == 32) && (strcmp(((mbuff != NULL) ? mbuff : ""), digest) == 0)) {
|
||||
ok = 1;
|
||||
hts_log_print(opt, LOG_DEBUG, "File not re-written (md5): %s", save);
|
||||
}
|
||||
freet(mbuff);
|
||||
}
|
||||
if (!ok) {
|
||||
{
|
||||
file_notify(opt, adr, fil, save, 1, 1, r->notmodified);
|
||||
*fp = filecreate(&opt->state.strc, save);
|
||||
if (*fp) {
|
||||
@@ -545,13 +508,7 @@ void hts_finish_html_file(httrackp *opt, cache_back *cache, htsblk *r,
|
||||
if (fcheck)
|
||||
hts_log_print(opt, LOG_ERROR, "* * Fatal write error, giving up");
|
||||
}
|
||||
} else {
|
||||
file_notify(opt, adr, fil, save, 0, 0, r->notmodified);
|
||||
filenote(&opt->state.strc, save, NULL);
|
||||
}
|
||||
if (cache->ndx)
|
||||
cache_writedata(cache->ndx, cache->dat, "//[HTML-MD5]//", save, digest,
|
||||
(int) strlen(digest));
|
||||
}
|
||||
|
||||
/* does it look like XML ? (SVG et al.) */
|
||||
|
||||
@@ -193,8 +193,7 @@ struct cache_back {
|
||||
/* */
|
||||
int type;
|
||||
int ro; /**< read-only: no new cache is written */
|
||||
FILE *dat, *ndx, *olddat; /**< new data, new index, old data files */
|
||||
char *use; /**< in-memory list of cached adr+fil keys */
|
||||
|
||||
FILE *lst; /**< file list, used for purge */
|
||||
FILE *txt; /**< human-readable file list (info) */
|
||||
char lastmodified[256];
|
||||
@@ -288,12 +287,12 @@ struct filecreate_params {
|
||||
|
||||
/* True if a new cache is being written (plain or zip backend). */
|
||||
HTS_STATIC int cache_writable(cache_back * cache) {
|
||||
return (cache != NULL && (cache->dat != NULL || cache->zipOutput != NULL));
|
||||
return (cache != NULL && cache->zipOutput != NULL);
|
||||
}
|
||||
|
||||
/* True if an old cache is available to read (plain or zip backend). */
|
||||
HTS_STATIC int cache_readable(cache_back * cache) {
|
||||
return (cache != NULL && (cache->olddat != NULL || cache->zipInput != NULL));
|
||||
return (cache != NULL && cache->zipInput != NULL);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1953,18 +1953,20 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
|
||||
char BIGSTK url[HTS_URLMAXSIZE * 2];
|
||||
char linepos[256];
|
||||
int pos;
|
||||
char *cacheNdx =
|
||||
readfile(fconcat
|
||||
(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt), StringBuff(opt->path_log),
|
||||
"hts-cache/new.ndx"));
|
||||
LLint cacheNdxLen = 0;
|
||||
char *cacheNdx = readfile2(
|
||||
fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt),
|
||||
StringBuff(opt->path_log), "hts-cache/new.ndx"),
|
||||
&cacheNdxLen);
|
||||
cache_init(&cache, opt); /* load cache */
|
||||
if (cacheNdx != NULL) {
|
||||
char firstline[256];
|
||||
char *a = cacheNdx;
|
||||
const char *const end = cacheNdx + cacheNdxLen;
|
||||
|
||||
a += cache_brstr(a, firstline, sizeof(firstline));
|
||||
a += cache_brstr(a, firstline, sizeof(firstline));
|
||||
while(a != NULL) {
|
||||
while (a != NULL && a < end) {
|
||||
a = strchr(a + 1, '\n'); /* start of line */
|
||||
if (a) {
|
||||
htsblk r;
|
||||
@@ -1972,15 +1974,15 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
|
||||
/* */
|
||||
a++;
|
||||
/* read "host/file" */
|
||||
a += binput(a, afs.af.adr, HTS_URLMAXSIZE);
|
||||
a += binput(a, afs.af.fil, HTS_URLMAXSIZE);
|
||||
a += cache_binput(a, end, afs.af.adr, HTS_URLMAXSIZE);
|
||||
a += cache_binput(a, end, afs.af.fil, HTS_URLMAXSIZE);
|
||||
url[0] = '\0';
|
||||
if (!link_has_authority(afs.af.adr))
|
||||
strcatbuff(url, "http://");
|
||||
strcatbuff(url, afs.af.adr);
|
||||
strcatbuff(url, afs.af.fil);
|
||||
/* read position */
|
||||
a += binput(a, linepos, 200);
|
||||
a += cache_binput(a, end, linepos, 200);
|
||||
sscanf(linepos, "%d", &pos);
|
||||
if (!hasFilter
|
||||
|| (strjoker(url, filter, NULL, NULL) != NULL)
|
||||
|
||||
@@ -48,6 +48,7 @@ Please visit our Website: http://www.httrack.com
|
||||
#include "htsbase.h"
|
||||
#include "htslib.h"
|
||||
#include <ctype.h>
|
||||
#include <stdint.h>
|
||||
/* END specific definitions */
|
||||
|
||||
// à partir d'un tableau de {"+*.toto","-*.zip","+*.tata"} définit si nom est autorisé
|
||||
@@ -120,16 +121,81 @@ int fa_strjoker_dual(int type, char **filters, int nfil, const char *nom1,
|
||||
return use1 ? jok1 : jok2;
|
||||
}
|
||||
|
||||
// supercomparateur joker (tm)
|
||||
// compare a et b (b=avec joker dedans), case insensitive [voir CI]
|
||||
// renvoi l'adresse de la première lettre de la chaine
|
||||
// (càd *[..]toto.. renvoi adresse de toto dans la chaine)
|
||||
// accepte les délires du genre www.*.*/ * / * truc*.*
|
||||
// cet algo est 'un peu' récursif mais ne consomme pas trop de tm
|
||||
// * = toute lettre
|
||||
// --?-- : spécifique à HTTrack et aux ?
|
||||
/* Failure memo for the recursive matcher: one bit per (chaine, joker) offset
|
||||
pair keeps star-heavy patterns polynomial instead of exponential (#501). */
|
||||
typedef struct strjoker_memo {
|
||||
const char *chaine0, *joker0; /* offsets are relative to these bases */
|
||||
size_t stride; /* strlen(joker0) + 1 */
|
||||
unsigned char *failed; /* failed-pair bitmap; NULL: no memo */
|
||||
} strjoker_memo;
|
||||
|
||||
static const char *strjoker_impl(const strjoker_memo *memo, const char *chaine,
|
||||
const char *joker, LLint *size,
|
||||
int *size_flag);
|
||||
|
||||
/* A pair that failed once fails forever (*size is only written on the success
|
||||
path), so record NULL results and cut the re-exploration. */
|
||||
static const char *strjoker_rec(const strjoker_memo *memo, const char *chaine,
|
||||
const char *joker, LLint *size,
|
||||
int *size_flag) {
|
||||
size_t bit = 0;
|
||||
const char *adr;
|
||||
|
||||
if (memo->failed) {
|
||||
bit = (size_t) (chaine - memo->chaine0) * memo->stride +
|
||||
(size_t) (joker - memo->joker0);
|
||||
if (memo->failed[bit >> 3] & (unsigned char) (1u << (bit & 7)))
|
||||
return NULL;
|
||||
}
|
||||
adr = strjoker_impl(memo, chaine, joker, size, size_flag);
|
||||
if (adr == NULL && memo->failed)
|
||||
memo->failed[bit >> 3] |= (unsigned char) (1u << (bit & 7));
|
||||
return adr;
|
||||
}
|
||||
|
||||
// wildcard comparator: match chaine against joker (pattern), case-insensitive
|
||||
// returns the address of the first matched letter past any leading joker
|
||||
// (ie. *[..]toto.. returns the address of toto within chaine), NULL on mismatch
|
||||
HTS_INLINE const char *strjoker(const char *chaine, const char *joker,
|
||||
LLint *size, int *size_flag) {
|
||||
strjoker_memo memo = {chaine, joker, 0, NULL};
|
||||
unsigned char stackbits[2048];
|
||||
hts_boolean onheap = HTS_FALSE;
|
||||
const char *adr;
|
||||
|
||||
if (chaine != NULL && joker != NULL) {
|
||||
const size_t n1 = strlen(chaine) + 1;
|
||||
|
||||
memo.stride = strlen(joker) + 1;
|
||||
if (n1 <= (SIZE_MAX - 7) / memo.stride) { // overflow-safe bitmap size
|
||||
const size_t bytes = (n1 * memo.stride + 7) / 8;
|
||||
|
||||
if (bytes <= sizeof(stackbits)) {
|
||||
memset(stackbits, 0, bytes);
|
||||
memo.failed = stackbits;
|
||||
} else {
|
||||
memo.failed = (unsigned char *) calloct(bytes, 1);
|
||||
onheap = memo.failed != NULL ? HTS_TRUE : HTS_FALSE;
|
||||
}
|
||||
}
|
||||
}
|
||||
adr = strjoker_rec(&memo, chaine, joker, size, size_flag);
|
||||
if (onheap)
|
||||
freet(memo.failed);
|
||||
return adr;
|
||||
}
|
||||
|
||||
/* Test-only oracle: the same matcher without the failure memo. */
|
||||
const char *strjoker_nomemo(const char *chaine, const char *joker, LLint *size,
|
||||
int *size_flag) {
|
||||
const strjoker_memo memo = {chaine, joker, 0, NULL};
|
||||
|
||||
return strjoker_rec(&memo, chaine, joker, size, size_flag);
|
||||
}
|
||||
|
||||
static const char *strjoker_impl(const strjoker_memo *memo, const char *chaine,
|
||||
const char *joker, LLint *size,
|
||||
int *size_flag) {
|
||||
if (strnotempty(joker) == 0) { // fin de chaine joker
|
||||
if (strnotempty(chaine) == 0) // fin aussi pour la chaine: ok
|
||||
return chaine;
|
||||
@@ -303,7 +369,7 @@ HTS_INLINE const char *strjoker(const char *chaine, const char *joker,
|
||||
|
||||
// tester sans le joker (pas ()+ mais ()*)
|
||||
if (!unique) {
|
||||
if ((adr = strjoker(chaine, joker + jmp, size, size_flag))) {
|
||||
if ((adr = strjoker_rec(memo, chaine, joker + jmp, size, size_flag))) {
|
||||
return adr;
|
||||
}
|
||||
}
|
||||
@@ -315,7 +381,8 @@ HTS_INLINE const char *strjoker(const char *chaine, const char *joker,
|
||||
max = strnotempty(chaine) ? 1 : 0; /* empty chaine: no char to eat */
|
||||
while(i < (int) max) {
|
||||
if (pass[(int) (unsigned char) chaine[i]]) { // caractère autorisé
|
||||
if ((adr = strjoker(chaine + i + 1, joker + jmp, size, size_flag))) {
|
||||
if ((adr = strjoker_rec(memo, chaine + i + 1, joker + jmp, size,
|
||||
size_flag))) {
|
||||
return adr;
|
||||
}
|
||||
i++;
|
||||
@@ -325,7 +392,8 @@ HTS_INLINE const char *strjoker(const char *chaine, const char *joker,
|
||||
|
||||
// tester chaîne vide
|
||||
if (i != max + 2) // avant c'est ok
|
||||
if ((adr = strjoker(chaine + max, joker + jmp, size, size_flag)))
|
||||
if ((adr = strjoker_rec(memo, chaine + max, joker + jmp, size,
|
||||
size_flag)))
|
||||
return adr;
|
||||
|
||||
return NULL; // perdu
|
||||
@@ -347,7 +415,7 @@ HTS_INLINE const char *strjoker(const char *chaine, const char *joker,
|
||||
// comparaison ok?
|
||||
if (ok) {
|
||||
// continuer la comparaison.
|
||||
if (strjoker(chaine + jmp, joker + jmp, size, size_flag))
|
||||
if (strjoker_rec(memo, chaine + jmp, joker + jmp, size, size_flag))
|
||||
return chaine; // retourner 1e lettre
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,10 @@ int fa_strjoker_dual(int type, char **filters, int nfil, const char *nom1,
|
||||
const char *nom2, LLint *size, int *size_flag, int *depth);
|
||||
HTS_INLINE const char *strjoker(const char *chaine, const char *joker, LLint * size,
|
||||
int *size_flag);
|
||||
/* strjoker() without the failure memo (exponential worst case); test-only
|
||||
oracle for the memoized matcher. */
|
||||
const char *strjoker_nomemo(const char *chaine, const char *joker, LLint *size,
|
||||
int *size_flag);
|
||||
const char *strjokerfind(const char *chaine, const char *joker);
|
||||
#endif
|
||||
|
||||
|
||||
@@ -422,9 +422,10 @@ typedef int T_SOC;
|
||||
#endif
|
||||
|
||||
/* Permission bits for created folders and files (mkdir and chmod).
|
||||
PROTECT_FOLDER is owner-only. With HTS_ACCESS set (the default) the ACCESS_
|
||||
modes also grant group/other read; otherwise they stay owner-only. */
|
||||
PROTECT_FOLDER/FILE are owner-only. With HTS_ACCESS set (the default) the
|
||||
ACCESS_ modes also grant group/other read; otherwise they stay owner-only. */
|
||||
#define HTS_PROTECT_FOLDER (S_IRUSR | S_IWUSR | S_IXUSR)
|
||||
#define HTS_PROTECT_FILE (S_IRUSR | S_IWUSR)
|
||||
|
||||
#if HTS_ACCESS
|
||||
#define HTS_ACCESS_FILE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
|
||||
|
||||
@@ -1562,7 +1562,7 @@ void treathead(t_cookie * cookie, const char *adr, const char *fil, htsblk * ret
|
||||
}
|
||||
// An empty/whitespace Content-Type value yields no token: keep the
|
||||
// sentinel default rather than reading an uninitialized tempo.
|
||||
if (sscanf(rcvd + p, "%s", tempo) == 1) {
|
||||
if (sscanf(rcvd + p, "%1099s", tempo) == 1) { // tempo[1100], server data
|
||||
if (strlen(tempo) < sizeof(retour->contenttype) - 2) // pas trop long!!
|
||||
strcpybuff(retour->contenttype, tempo);
|
||||
else
|
||||
@@ -1657,11 +1657,11 @@ void treathead(t_cookie * cookie, const char *adr, const char *fil, htsblk * ret
|
||||
if (a)
|
||||
*a = '\0';
|
||||
}
|
||||
sscanf(a, "%s", tempo);
|
||||
if (strlen(tempo) < 64) // pas trop long!!
|
||||
// Bound to tempo[1100]; no token => leave empty, don't read uninit tempo.
|
||||
if (sscanf(a, "%1099s", tempo) == 1 && strlen(tempo) < 64)
|
||||
strcpybuff(retour->contentencoding, tempo);
|
||||
else
|
||||
retour->contentencoding[0] = '\0'; // erreur
|
||||
retour->contentencoding[0] = '\0';
|
||||
#if HTS_USEZLIB
|
||||
/* Check known encodings */
|
||||
if (retour->contentencoding[0]) {
|
||||
|
||||
@@ -2007,7 +2007,7 @@ int htsparse(htsmoduleStruct * str, htsmoduleStructExtended * stre) {
|
||||
|
||||
/* Unescape/escape %20 and other */
|
||||
{
|
||||
// Note: always true (iso-8859-1 as default)
|
||||
// NULL when UTF-8 conversion is off (-%T0)
|
||||
const char *const charset = str->page_charset_;
|
||||
const int hasCharset = charset != NULL
|
||||
&& *charset != '\0';
|
||||
@@ -2030,11 +2030,12 @@ int htsparse(htsmoduleStruct * str, htsmoduleStructExtended * stre) {
|
||||
|
||||
// Force to encode non-printable chars (should never happend)
|
||||
escape_remove_control(lien);
|
||||
|
||||
// charset conversion for the URI filename,
|
||||
// and not already UTF-8
|
||||
// (note: not for the query string!)
|
||||
if (hasCharset && !hts_isCharsetUTF8(charset)) {
|
||||
|
||||
// charset conversion for the URI filename (not the query
|
||||
// string), unless the bytes already are valid UTF-8:
|
||||
// converting those would double-encode them (#180)
|
||||
if (hasCharset && !hts_isCharsetUTF8(charset) &&
|
||||
!hts_isStringUTF8(lien, strlen(lien))) {
|
||||
char *const s = hts_convertStringToUTF8(lien, strlen(lien), charset);
|
||||
if (s != NULL) {
|
||||
hts_log_print(opt, LOG_DEBUG,
|
||||
|
||||
@@ -46,6 +46,7 @@ Please visit our Website: http://www.httrack.com
|
||||
#include "htsdefines.h"
|
||||
#include "htslib.h"
|
||||
#include "htsparse.h"
|
||||
#include "htscache.h"
|
||||
#include "htscache_selftest.h"
|
||||
#include "htsdns_selftest.h"
|
||||
#include "htscharset.h"
|
||||
@@ -620,6 +621,105 @@ static int st_filtersize(httrackp *opt, int argc, char **argv) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Mime-type filter verdict via fa_strjoker(type=1): only mime: rules apply. */
|
||||
static int st_filtermime(httrackp *opt, int argc, char **argv) {
|
||||
int verdict;
|
||||
|
||||
(void) opt;
|
||||
if (argc < 2) {
|
||||
fprintf(stderr, "filtermime: needs <mime> <filter> [filter...]\n");
|
||||
return 1;
|
||||
}
|
||||
verdict = fa_strjoker(1, &argv[1], argc - 1, argv[0], NULL, NULL, NULL);
|
||||
printf("verdict=%s\n", verdict > 0 ? "allowed"
|
||||
: verdict < 0 ? "forbidden"
|
||||
: "unknown");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* SplitMix64: deterministic, platform-independent case generator. */
|
||||
static uint64_t st_mix64(uint64_t *state) {
|
||||
uint64_t z = (*state += UINT64_C(0x9E3779B97F4A7C15));
|
||||
|
||||
z = (z ^ (z >> 30)) * UINT64_C(0xBF58476D1CE4B5B9);
|
||||
z = (z ^ (z >> 27)) * UINT64_C(0x94D049BB133111EB);
|
||||
return z ^ (z >> 31);
|
||||
}
|
||||
|
||||
/* Differential test: memoized strjoker vs the no-memo oracle on seeded random
|
||||
pattern/subject/size cases must agree on result, *size and *size_flag. */
|
||||
static int st_filtermemo(httrackp *opt, int argc, char **argv) {
|
||||
static const char *const pieces[] = {
|
||||
"a", "b", "A", "c", ".", "/",
|
||||
"?", "*", "*[a]", "*[ab]", "*[a-c]", "*[A-Z]",
|
||||
"*[<5]", "*[>5]", "*(a)", "*(a,b)", "*[file]", "*[path]",
|
||||
"*[name]", "*[param]", "*[]", "*[\\a]", "*[a-"};
|
||||
static const char subject_chars[] = "abAc./?";
|
||||
uint64_t rng = UINT64_C(0x501);
|
||||
int iters = 20000, matched = 0, unmatched = 0;
|
||||
int it;
|
||||
|
||||
(void) opt;
|
||||
if (argc >= 1)
|
||||
sscanf(argv[0], "%d", &iters);
|
||||
for (it = 0; it < iters; it++) {
|
||||
char pat[64], str[16];
|
||||
size_t pl = 0;
|
||||
const int npieces = (int) (st_mix64(&rng) % 5);
|
||||
const int slen = (int) (st_mix64(&rng) % 13);
|
||||
const int szsel = (int) (st_mix64(&rng) % 4); /* none/unknown/3/20 */
|
||||
LLint sz1, sz2, off1, off2;
|
||||
int i, flag1 = 0, flag2 = 0;
|
||||
char *hpat, *hstr;
|
||||
const char *adr;
|
||||
|
||||
for (i = 0; i < npieces; i++) {
|
||||
const char *p =
|
||||
pieces[st_mix64(&rng) % (sizeof(pieces) / sizeof(pieces[0]))];
|
||||
const size_t len = strlen(p);
|
||||
|
||||
if (len >= sizeof(pat) - pl)
|
||||
break;
|
||||
memcpy(pat + pl, p, len);
|
||||
pl += len;
|
||||
}
|
||||
pat[pl] = '\0';
|
||||
for (i = 0; i < slen; i++)
|
||||
str[i] = subject_chars[st_mix64(&rng) % (sizeof(subject_chars) - 1)];
|
||||
str[slen] = '\0';
|
||||
sz1 = sz2 = (szsel <= 1) ? -1 : (szsel == 2) ? 3 : 20;
|
||||
/* exact-size heap copies per run so a sanitizer traps any over-read */
|
||||
hpat = strdupt(pat);
|
||||
hstr = strdupt(str);
|
||||
adr = strjoker(hstr, hpat, szsel ? &sz1 : NULL, szsel ? &flag1 : NULL);
|
||||
off1 = adr != NULL ? (LLint) (adr - hstr) : -1;
|
||||
freet(hpat);
|
||||
freet(hstr);
|
||||
hpat = strdupt(pat);
|
||||
hstr = strdupt(str);
|
||||
adr =
|
||||
strjoker_nomemo(hstr, hpat, szsel ? &sz2 : NULL, szsel ? &flag2 : NULL);
|
||||
off2 = adr != NULL ? (LLint) (adr - hstr) : -1;
|
||||
freet(hpat);
|
||||
freet(hstr);
|
||||
if (off1 != off2 || sz1 != sz2 || flag1 != flag2) {
|
||||
printf("filtermemo MISMATCH pat=[%s] str=[%s] szsel=%d: "
|
||||
"off %d/%d size %d/%d flag %d/%d\n",
|
||||
pat, str, szsel, (int) off1, (int) off2, (int) sz1, (int) sz2,
|
||||
flag1, flag2);
|
||||
return 1;
|
||||
}
|
||||
if (off1 >= 0)
|
||||
matched++;
|
||||
else
|
||||
unmatched++;
|
||||
}
|
||||
/* both polarities must actually occur or the test proves nothing */
|
||||
assertf(matched > 0 && unmatched > 0);
|
||||
printf("filtermemo: %d cases OK\n", iters);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Merged two-form filter verdict via fa_strjoker_dual (see htsfilters.h). */
|
||||
static int st_filterdual(httrackp *opt, int argc, char **argv) {
|
||||
int depth = -1, verdict;
|
||||
@@ -688,6 +788,30 @@ static int st_charset(httrackp *opt, int argc, char **argv) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int st_metacharset(httrackp *opt, int argc, char **argv) {
|
||||
char *s;
|
||||
|
||||
(void) opt;
|
||||
if (argc < 1) {
|
||||
fprintf(stderr, "metacharset: needs an html string\n");
|
||||
return 1;
|
||||
}
|
||||
s = hts_getCharsetFromMeta(argv[0], strlen(argv[0]));
|
||||
printf("%s\n", s != NULL ? s : "(none)");
|
||||
freet(s);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int st_isutf8(httrackp *opt, int argc, char **argv) {
|
||||
(void) opt;
|
||||
if (argc < 1) {
|
||||
fprintf(stderr, "isutf8: needs a string\n");
|
||||
return 1;
|
||||
}
|
||||
printf("%d\n", hts_isStringUTF8(argv[0], strlen(argv[0])) ? 1 : 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int st_idna_encode(httrackp *opt, int argc, char **argv) {
|
||||
char *s;
|
||||
|
||||
@@ -1163,6 +1287,26 @@ static int st_header(httrackp *opt, int argc, char **argv) {
|
||||
treathead(NULL, "www.example.com", "/", &r, line);
|
||||
}
|
||||
printf("contenttype=%s cdispo=%s\n", r.contenttype, r.cdispo);
|
||||
printf("contentencoding=%s\n", r.contentencoding);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* An over-long header value must not overflow treathead's tempo[1100]. */
|
||||
static int st_headerlong(httrackp *opt, int argc, char **argv) {
|
||||
htsblk r;
|
||||
char BIGSTK line[HTS_URLMAXSIZE * 2];
|
||||
const char *const name = argc >= 1 ? argv[0] : "Content-Type:";
|
||||
const int pad = 1500; /* > tempo[1100] */
|
||||
size_t n;
|
||||
|
||||
(void) opt;
|
||||
memset(&r, 0, sizeof(r));
|
||||
n = (size_t) snprintf(line, sizeof(line), "%s ", name);
|
||||
memset(line + n, 'a', pad);
|
||||
line[n + pad] = '\0';
|
||||
treathead(NULL, "www.example.com", "/", &r, line);
|
||||
printf("contenttype_len=%d contentencoding_len=%d\n",
|
||||
(int) strlen(r.contenttype), (int) strlen(r.contentencoding));
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1370,6 +1514,78 @@ static int st_cache(httrackp *opt, int argc, char **argv) {
|
||||
return err;
|
||||
}
|
||||
|
||||
/* A corrupt cache index (.ndx) must not walk the length-prefixed scan past
|
||||
the buffer. Checks the two primitives the loader is built from. */
|
||||
static int st_cacheindex(httrackp *opt, int argc, char **argv) {
|
||||
int fail = 0;
|
||||
|
||||
(void) opt;
|
||||
(void) argc;
|
||||
(void) argv;
|
||||
|
||||
/* A length prefix that overstates the bytes present must bound the advance
|
||||
to the buffer, not trust the declared length. */
|
||||
{
|
||||
static const char src[] = "32768\nCACHE-1.1";
|
||||
const size_t len = sizeof(src) - 1;
|
||||
char *buf = malloct(len + 1);
|
||||
char s[256];
|
||||
int off;
|
||||
|
||||
memcpy(buf, src, len + 1);
|
||||
off = cache_brstr(buf, s, sizeof(s));
|
||||
if (off > (int) len) {
|
||||
printf("cacheindex: over-advance off=%d len=%d\n", off, (int) len);
|
||||
fail = 1;
|
||||
}
|
||||
if (strcmp(s, "CACHE-1.1") != 0) {
|
||||
printf("cacheindex: value=%s\n", s);
|
||||
fail = 1;
|
||||
}
|
||||
freet(buf);
|
||||
}
|
||||
|
||||
/* cache_binput reads a field while in bounds, but refuses one starting at
|
||||
or past end-of-buffer. */
|
||||
{
|
||||
char buf[8] = "ab\ncd";
|
||||
const char *const end = buf + 5;
|
||||
char s[16];
|
||||
|
||||
if (cache_binput(buf, end, s, sizeof(s)) != 3 || strcmp(s, "ab") != 0)
|
||||
fail = 1; /* normal read: "ab" then the '\n', 3 bytes consumed */
|
||||
if (cache_binput(end, end, s, sizeof(s)) != 0 || s[0] != '\0')
|
||||
fail = 1;
|
||||
}
|
||||
|
||||
/* Drive the full loader scan over a truncated index: a declared length that
|
||||
overshoots plus a half-written entry. ASan aborts here on the pre-fix
|
||||
scan; the cursor must never leave the buffer. */
|
||||
{
|
||||
static const char src[] = "9\nCACHE-1.1\n99\nwww.example.com\n/a";
|
||||
const size_t len = sizeof(src) - 1;
|
||||
char *buf = malloct(len + 1);
|
||||
const char *const end = buf + len;
|
||||
char line[256];
|
||||
char *a = buf;
|
||||
|
||||
memcpy(buf, src, len + 1);
|
||||
a += cache_brstr(a, line, sizeof(line));
|
||||
a += cache_brstr(a, line, sizeof(line));
|
||||
while (a != NULL && a < end) {
|
||||
a = strchr(a + 1, '\n');
|
||||
if (a == NULL)
|
||||
break;
|
||||
a++;
|
||||
a += cache_binput(a, end, line, sizeof(line));
|
||||
}
|
||||
freet(buf);
|
||||
}
|
||||
|
||||
printf("cacheindex: %s\n", fail ? "FAIL" : "OK");
|
||||
return fail;
|
||||
}
|
||||
|
||||
static int st_cache_golden(httrackp *opt, int argc, char **argv) {
|
||||
int regen, err;
|
||||
|
||||
@@ -1407,6 +1623,18 @@ static int st_cache_corrupt(httrackp *opt, int argc, char **argv) {
|
||||
return err;
|
||||
}
|
||||
|
||||
static int st_cache_legacy(httrackp *opt, int argc, char **argv) {
|
||||
int err;
|
||||
|
||||
if (argc < 1) {
|
||||
fprintf(stderr, "cache-legacy: needs a directory\n");
|
||||
return 1;
|
||||
}
|
||||
err = cache_legacy_refused_selftest(opt, argv[0]);
|
||||
printf("cache-legacy: %s\n", err ? "FAIL" : "OK");
|
||||
return err;
|
||||
}
|
||||
|
||||
static int st_reconcile(httrackp *opt, int argc, char **argv) {
|
||||
int err;
|
||||
|
||||
@@ -1458,6 +1686,26 @@ static int st_cookies(httrackp *opt, int argc, char **argv) {
|
||||
err = 1;
|
||||
if (strstr(hdr, "junk") != NULL) // wrong-domain cookie leaked
|
||||
err = 1;
|
||||
#ifndef _WIN32
|
||||
/* the jar holds live session cookies: cookie_save must keep it 0600 */
|
||||
{
|
||||
const char *jar = "st-cookies-jar.txt";
|
||||
struct stat st;
|
||||
|
||||
(void) UNLINK(jar);
|
||||
assertf(cookie_save(&cookie, jar) == 0);
|
||||
assertf(stat(jar, &st) == 0);
|
||||
assertf((st.st_mode & 07777) == HTS_PROTECT_FILE);
|
||||
assertf(st.st_size > 0); /* mode-only checks would pass an empty jar */
|
||||
/* a pre-existing world-readable jar must be tightened, not kept */
|
||||
assertf(chmod(jar, 0644) == 0);
|
||||
assertf(cookie_save(&cookie, jar) == 0);
|
||||
assertf(stat(jar, &st) == 0);
|
||||
assertf((st.st_mode & 07777) == HTS_PROTECT_FILE);
|
||||
assertf(st.st_size > 0);
|
||||
(void) UNLINK(jar);
|
||||
}
|
||||
#endif
|
||||
printf("cookie-header: %s\n", err ? "FAIL" : "OK");
|
||||
if (err)
|
||||
printf(" got: %s\n", hdr);
|
||||
@@ -2154,6 +2402,10 @@ static const struct selftest_entry {
|
||||
{"filtersize", "<size> <string> <filter>...",
|
||||
"size-aware filter verdict (negative size = unknown/scan time)",
|
||||
st_filtersize},
|
||||
{"filtermime", "<mime> <filter>...",
|
||||
"mime-type filter verdict (fa_strjoker type=1)", st_filtermime},
|
||||
{"filtermemo", "[iterations]",
|
||||
"memoized vs unmemoized matcher differential", st_filtermemo},
|
||||
{"filterdual", "<string1> <string2> <filter>...",
|
||||
"merged two-form filter verdict (fa_strjoker_dual)", st_filterdual},
|
||||
{"simplify", "<path>", "collapse ./ and ../ in a path", st_simplify},
|
||||
@@ -2166,6 +2418,9 @@ static const struct selftest_entry {
|
||||
{"mime", "<filename>", "MIME type for a filename", st_mime},
|
||||
{"charset", "<charset> <string>",
|
||||
"convert a string to UTF-8 from a charset", st_charset},
|
||||
{"metacharset", "<html>", "extract the <meta> charset from an HTML page",
|
||||
st_metacharset},
|
||||
{"isutf8", "<string>", "is the string valid UTF-8 (1/0)", st_isutf8},
|
||||
{"idna-encode", "<host>", "encode a hostname to IDNA/punycode",
|
||||
st_idna_encode},
|
||||
{"idna-decode", "<host>", "decode an IDNA/punycode hostname",
|
||||
@@ -2185,17 +2440,24 @@ static const struct selftest_entry {
|
||||
{"identurl", "<url>", "split an absolute URL into (adr, fil)", st_identurl},
|
||||
{"header", "<raw-header-line> ...", "response header-line parsing",
|
||||
st_header},
|
||||
{"headerlong", "[header-name:]",
|
||||
"over-long header value must not overflow the parse scratch",
|
||||
st_headerlong},
|
||||
{"savename", "<fil> <content-type> [key=value ...]",
|
||||
"local save-name for a URL", st_savename},
|
||||
{"sniff", "<content-type> <hex:..|text>", "MIME magic consistency",
|
||||
st_sniff},
|
||||
{"cache", "<dir>", "cache read/write round-trip self-test", st_cache},
|
||||
{"cacheindex", "", "cache-index (.ndx) parse must stay in bounds",
|
||||
st_cacheindex},
|
||||
{"cache-golden", "<dir> [regen]", "frozen cache-format read self-test",
|
||||
st_cache_golden},
|
||||
{"cache-writefail", "<dir>", "cache write-failure handling self-test",
|
||||
st_cache_writefail},
|
||||
{"reconcile", "<dir>", "cache generation reconcile policy self-test",
|
||||
st_reconcile},
|
||||
{"cache-legacy", "<dir>", "pre-3.31 legacy cache refusal self-test",
|
||||
st_cache_legacy},
|
||||
{"cache-corrupt", "<dir>", "cache read-side corruption self-test",
|
||||
st_cache_corrupt},
|
||||
{"dns", "", "DNS resolver/cache self-test", st_dns},
|
||||
|
||||
10
tests/01_engine-cacheindex.test
Executable file
10
tests/01_engine-cacheindex.test
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Cache-index (.ndx) parse must stay inside the buffer on a corrupt length
|
||||
# prefix (ASan aborts here on the pre-fix binary; the OK check gates the
|
||||
# non-sanitized build too).
|
||||
|
||||
test "$(httrack -O /dev/null -#test=cacheindex)" == 'cacheindex: OK'
|
||||
@@ -36,3 +36,64 @@ test "$(httrack -O /dev/null -#test=charset 'no-such-charset-xyz' 'café' 2>/dev
|
||||
# malformed UTF-8 (lone continuation byte, truncated lead byte) must not crash
|
||||
runs 'utf-8' $'\x80'
|
||||
runs 'utf-8' $'\xc3'
|
||||
|
||||
# hts_getCharsetFromMeta: <meta> charset extraction, HTML5 and legacy forms.
|
||||
# -#test=metacharset <html> prints the charset, or "(none)".
|
||||
meta() {
|
||||
test "$(httrack -O /dev/null -#test=metacharset "$1")" == "$2" || exit 1
|
||||
}
|
||||
|
||||
# HTML5 form, quoting/spacing flavors
|
||||
meta '<meta charset="utf-8">' 'utf-8'
|
||||
meta "<meta charset='utf-8'>" 'utf-8'
|
||||
meta '<meta charset=utf-8>' 'utf-8'
|
||||
meta '<meta charset=utf-8 />' 'utf-8'
|
||||
meta '<meta charset=utf-8/>' 'utf-8'
|
||||
# charset labels are case-insensitive downstream; only require case-insensitive match
|
||||
test "$(httrack -O /dev/null -#test=metacharset '<META CHARSET=UTF-8>' | tr '[:upper:]' '[:lower:]')" == 'utf-8' || exit 1
|
||||
meta '<html><head><meta charset = "utf-8" ></head>' 'utf-8'
|
||||
|
||||
# legacy http-equiv form, attributes in any order
|
||||
meta '<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">' 'iso-8859-1'
|
||||
meta '<meta content="text/html; charset=gb2312" http-equiv="content-type">' 'gb2312'
|
||||
meta '<meta http-equiv=Content-Type content="text/html;charset=euc-jp">' 'euc-jp'
|
||||
meta '<meta http-equiv="Content-Type" content="text/html; charset = koi8-r ">' 'koi8-r'
|
||||
meta '<meta http-equiv="Content-Type" content="text/html; charset=utf-8;">' 'utf-8'
|
||||
meta '<meta http-equiv=content-type content=text/html;charset=utf-8 />' 'utf-8'
|
||||
meta '<meta http-equiv=content-type content=text/html;charset=utf-8>' 'utf-8'
|
||||
|
||||
# first resolvable meta wins; unrelated metas are skipped
|
||||
meta '<meta name="viewport" content="width=1"><meta charset="koi8-r">' 'koi8-r'
|
||||
meta '<meta charset="utf-8"><meta http-equiv="Content-Type" content="text/html; charset=gb2312">' 'utf-8'
|
||||
|
||||
# duplicate attribute: first wins, as in browsers
|
||||
meta '<meta http-equiv="Content-Type" content="text/html; charset=big5" http-equiv="refresh">' 'big5'
|
||||
|
||||
# negatives: wrong tag/attribute, empty value
|
||||
meta '<p>x</p>' '(none)'
|
||||
meta '<meta name="desc" content="charset=bogus">' '(none)'
|
||||
meta '<meta http-equiv="refresh" content="0; url=x.html">' '(none)'
|
||||
meta '<body charset="utf-8">' '(none)'
|
||||
meta '<meta charset="">' '(none)'
|
||||
|
||||
# hostile truncations: no crash, no match
|
||||
meta '<meta charset="utf-8' '(none)'
|
||||
meta '<meta charset=' '(none)'
|
||||
meta '<meta ' '(none)'
|
||||
meta '<meta' '(none)'
|
||||
|
||||
# hts_isStringUTF8: valid-UTF-8 probe (guards the #180 link conversion).
|
||||
utf8() {
|
||||
test "$(httrack -O /dev/null -#test=isutf8 "$1")" == "$2" || exit 1
|
||||
}
|
||||
utf8 'plain' '1'
|
||||
utf8 'café' '1'
|
||||
utf8 '统计' '1'
|
||||
utf8 "$(printf 'caf\xe9')" '0' # latin-1 accent
|
||||
utf8 "$(printf '\xc3')" '0' # truncated lead byte
|
||||
utf8 "$(printf '\xa9')" '0' # lone continuation byte
|
||||
utf8 "$(printf '\xf0\x9f\x98\x80')" '1' # 4-byte emoji
|
||||
utf8 "$(printf '\xc0\xaf')" '0' # overlong (latin-1 "À¯" must stay convertible)
|
||||
utf8 "$(printf '\xed\xa0\x80')" '0' # UTF-16 surrogate
|
||||
utf8 "$(printf '\xf4\x90\x80\x80')" '0' # above U+10FFFF
|
||||
utf8 "$(printf '\xf8\x88\x80\x80\x80')" '0' # legacy 5-byte form
|
||||
|
||||
@@ -131,3 +131,64 @@ match '*.aspx' 'page.aspx?y=2'
|
||||
# empty subject against a unique-match allow-all pattern (heap over-read regress)
|
||||
nomatch_empty '*(('
|
||||
nomatch_empty '**(('
|
||||
|
||||
# near-miss negatives: word order, exact tails, range boundaries, class case
|
||||
nomatch 'foo*bar' 'foobaz'
|
||||
nomatch '*foo*bar' 'barfoo' # both words present, wrong order
|
||||
nomatch '*.gif' 'a.gifx' # trailing junk after the extension
|
||||
nomatch '*.gif' 'a.gi'
|
||||
nomatch '*[b-y]' 'a'
|
||||
nomatch '*[b-y]' 'z'
|
||||
nomatch '*[a-z].txt' 'B.txt' # classes are case-sensitive...
|
||||
match 'FoO*' 'foobar' # ...literals are not
|
||||
|
||||
# a star may match an empty run, but never skip a required literal
|
||||
match '*[0-9].gif' '.gif'
|
||||
nomatch '*[0-9].gif' 'x.gif'
|
||||
|
||||
# a descending range *[c-a] is an empty class, not c..a reversed
|
||||
match '*[c-a]x' 'x'
|
||||
nomatch '*[c-a]x' 'cx'
|
||||
|
||||
# *[param] is all-or-nothing on '?': spans everything after one, else nothing
|
||||
match 'a*[param]' 'a?x=1'
|
||||
nomatch 'a*[param]' 'ab?x=1'
|
||||
|
||||
# the latest matching rule wins, regardless of sign
|
||||
fsize 'verdict=forbidden size_flag=0' -1 a.gif '+*.gif' '-*.gif'
|
||||
fsize 'verdict=allowed size_flag=0' -1 a.gif '-*.gif' '+*.gif'
|
||||
|
||||
# a failed size test sets size_flag but leaves the rule unmatched
|
||||
fsize 'verdict=unknown size_flag=1' 5 foo.jpg '-*.jpg*[>10]'
|
||||
|
||||
# mime filters (-#test=filtermime = fa_strjoker type=1): only mime: rules
|
||||
# apply to a mime type, and mime: rules never apply to a URL
|
||||
fmime() {
|
||||
local want="$1"
|
||||
shift
|
||||
test "$(httrack -O /dev/null -#test=filtermime "$@")" == "verdict=$want" || exit 1
|
||||
}
|
||||
fmime allowed 'image/png' '+mime:image/*'
|
||||
fmime unknown 'text/html' '+mime:image/*'
|
||||
fmime forbidden 'image/png' '+mime:image/*' '-mime:image/png' # last wins
|
||||
fmime allowed 'image/png' '-mime:image/png' '+mime:image/*'
|
||||
fmime unknown 'image/png' '+image/*' # URL rule: skipped
|
||||
fsize 'verdict=unknown size_flag=0' -1 'image/png' '+mime:image/*' # and vice versa
|
||||
|
||||
# memoized vs no-memo differential over seeded random patterns and subjects,
|
||||
# matching and non-matching (the self-test asserts both polarities occurred)
|
||||
test "$(httrack -O /dev/null -#test=filtermemo)" == "filtermemo: 20000 cases OK" || exit 1
|
||||
|
||||
# star-heavy non-match must stay polynomial (#501); 400 chars also exercises
|
||||
# the heap-allocated memo. timeout (where available) turns a hang into a FAIL.
|
||||
with_timeout() {
|
||||
if command -v timeout >/dev/null; then timeout 60 "$@"; else "$@"; fi
|
||||
}
|
||||
pat=$(printf '*[a]%.0s' $(seq 1 12))b
|
||||
subj=$(printf 'a%.0s' $(seq 1 400))
|
||||
test "$(with_timeout httrack -O /dev/null -#test=filter "$pat" "$subj")" == "$subj does NOT match $pat" || exit 1
|
||||
|
||||
# a later star's class dead-end must re-extend the earlier star, not give up
|
||||
# (guards against a single-backtrack-point matcher rewrite)
|
||||
match '*[aX]X*[a]Y' 'aXaXaY'
|
||||
nomatch '*[aX]X*[a]Y' 'aXbXaY'
|
||||
|
||||
@@ -27,3 +27,16 @@ hdr 'contenttype= cdispo=report.pdf' \
|
||||
# Path components in the filename are dropped on the wire (RFC 2616).
|
||||
hdr 'contenttype= cdispo=evil.pdf' \
|
||||
'Content-Disposition: attachment; filename="../../evil.pdf"'
|
||||
|
||||
# An over-long header value must not overflow the parse scratch (ASan aborts
|
||||
# here on the pre-fix binary).
|
||||
test "$(httrack -O /dev/null -#test=headerlong 'Content-Type:')" \
|
||||
== 'contenttype_len=32 contentencoding_len=0'
|
||||
test "$(httrack -O /dev/null -#test=headerlong 'Content-Encoding:')" \
|
||||
== 'contenttype_len=0 contentencoding_len=0'
|
||||
|
||||
# An empty Content-Encoding yields no token: leave it empty, don't read the
|
||||
# uninitialized scratch (MSan aborts here on the pre-fix binary).
|
||||
enc() { httrack -O /dev/null -#test=header "$1" | grep '^contentencoding='; }
|
||||
test "$(enc 'Content-Encoding:')" == 'contentencoding='
|
||||
test "$(enc 'Content-Encoding: gzip')" == 'contentencoding=gzip'
|
||||
|
||||
23
tests/01_zlib-cache-legacy.test
Normal file
23
tests/01_zlib-cache-legacy.test
Normal file
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# The pre-3.31 .dat/.ndx cache import was removed: cache_init must refuse the
|
||||
# legacy pair and leave the files untouched (httrack -#test=cache-legacy <dir>).
|
||||
|
||||
set -eu
|
||||
|
||||
dir=$(mktemp -d)
|
||||
trap 'rm -rf "$dir"' EXIT
|
||||
|
||||
# the refusal errors land on stdout (no log file); pin them and the verdict
|
||||
out=$(httrack -#test=cache-legacy "$dir" 2>/dev/null)
|
||||
cnt=$(printf '%s\n' "$out" | grep -c 'no longer supported' || true)
|
||||
test "$cnt" = 2 || {
|
||||
echo "expected 2 refusal messages, got $cnt" >&2
|
||||
exit 1
|
||||
}
|
||||
out=$(printf '%s\n' "$out" | tail -n1)
|
||||
|
||||
test "$out" = "cache-legacy: OK" || {
|
||||
echo "expected 'cache-legacy: OK', got: $out" >&2
|
||||
exit 1
|
||||
}
|
||||
@@ -22,9 +22,9 @@ bash crawl-test.sh \
|
||||
httrack http://ut.httrack.com/unicode-links/utf8.html
|
||||
|
||||
bash crawl-test.sh \
|
||||
--errors 4 --files 7 \
|
||||
--found ut.httrack.com/unicode-links/café3860.html \
|
||||
--found ut.httrack.com/unicode-links/café9fa8.html \
|
||||
--errors 2 --files 9 \
|
||||
--found ut.httrack.com/unicode-links/café3860.html \
|
||||
--found ut.httrack.com/unicode-links/café9fa8.html \
|
||||
--found ut.httrack.com/unicode-links/café30f4.html \
|
||||
--found ut.httrack.com/unicode-links/café5e1f.html \
|
||||
--found ut.httrack.com/unicode-links/café7b30.html \
|
||||
|
||||
21
tests/41_local-utf8-link.test
Normal file
21
tests/41_local-utf8-link.test
Normal file
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# #180: raw non-ASCII hrefs must reach the wire UTF-8 percent-encoded whatever
|
||||
# the declared page charset; each PDF exists only at its exact UTF-8 path.
|
||||
|
||||
set -euo pipefail
|
||||
: "${top_srcdir:=..}"
|
||||
|
||||
bash "$top_srcdir/tests/local-crawl.sh" --errors 0 --rerun \
|
||||
--found 'charset/header/统计大数据服务平台.pdf' \
|
||||
--found 'charset/meta5/统计大数据服务平台.pdf' \
|
||||
--found 'charset/metaeq/统计大数据服务平台.pdf' \
|
||||
--found 'charset/none/统计大数据服务平台.pdf' \
|
||||
--found 'charset/latin1hdr/统计大数据服务平台.pdf' \
|
||||
--found 'charset/latin1real/café.pdf' \
|
||||
--found 'charset/metalatin1/déjà.pdf' \
|
||||
--found 'charset/latin1ovl/À¡x.pdf' \
|
||||
--found 'charset/priority/nuée.pdf' \
|
||||
--found 'charset/preenc/统计大数据服务平台.pdf' \
|
||||
--found 'charset/bom/统计大数据服务平台.pdf' \
|
||||
httrack 'BASEURL/charset/index.html'
|
||||
@@ -38,6 +38,7 @@ TESTS = \
|
||||
01_engine-filterdual.test \
|
||||
01_engine-ftp-line.test \
|
||||
01_engine-ftp-userpass.test \
|
||||
01_engine-cacheindex.test \
|
||||
01_engine-hashtable.test \
|
||||
01_engine-header.test \
|
||||
01_engine-idna.test \
|
||||
@@ -67,6 +68,7 @@ TESTS = \
|
||||
01_zlib-acceptencoding.test \
|
||||
01_zlib-cache.test \
|
||||
01_zlib-cache-corrupt.test \
|
||||
01_zlib-cache-legacy.test \
|
||||
01_zlib-cache-golden.test \
|
||||
01_zlib-cache-writefail.test \
|
||||
01_zlib-savename-cached.test \
|
||||
@@ -107,6 +109,7 @@ TESTS = \
|
||||
37_local-cache-outage.test \
|
||||
38_local-update-304.test \
|
||||
39_local-delayed-cancel.test \
|
||||
40_local-why.test
|
||||
40_local-why.test \
|
||||
41_local-utf8-link.test
|
||||
|
||||
CLEANFILES = check-network_sh.cache
|
||||
|
||||
@@ -660,6 +660,97 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
def route_intl_page(self):
|
||||
self.send_raw(b"<html><body>accented page</body></html>\n", "text/html")
|
||||
|
||||
# Raw non-ASCII href matrix (#180): each variant declares the page charset
|
||||
# differently; the PDF exists only at its exact UTF-8 path, so a
|
||||
# mis-decoded link 404s.
|
||||
CHARSET_CJK = "统计大数据服务平台.pdf"
|
||||
# variant -> (index Content-Type, <head> bytes, href bytes, pdf name)
|
||||
CHARSET_VARIANTS = {
|
||||
"header": ("text/html; charset=utf-8", b"", CHARSET_CJK.encode(), CHARSET_CJK),
|
||||
"meta5": (
|
||||
"text/html",
|
||||
b'<meta charset="utf-8">',
|
||||
CHARSET_CJK.encode(),
|
||||
CHARSET_CJK,
|
||||
),
|
||||
"metaeq": (
|
||||
"text/html",
|
||||
b'<meta http-equiv="Content-Type" content="text/html; charset=utf-8">',
|
||||
CHARSET_CJK.encode(),
|
||||
CHARSET_CJK,
|
||||
),
|
||||
"none": ("text/html", b"", CHARSET_CJK.encode(), CHARSET_CJK),
|
||||
"latin1hdr": (
|
||||
"text/html; charset=iso-8859-1",
|
||||
b"",
|
||||
CHARSET_CJK.encode(),
|
||||
CHARSET_CJK,
|
||||
),
|
||||
# genuine latin-1 href: the charset conversion must still apply
|
||||
"latin1real": (
|
||||
"text/html; charset=iso-8859-1",
|
||||
b"",
|
||||
"café.pdf".encode("latin-1"),
|
||||
"café.pdf",
|
||||
),
|
||||
# latin-1 declared by META only: the meta parser is load-bearing
|
||||
"metalatin1": (
|
||||
"text/html",
|
||||
b'<meta charset="iso-8859-1">',
|
||||
"déjà.pdf".encode("latin-1"),
|
||||
"déjà.pdf",
|
||||
),
|
||||
# latin-1 bytes that form an overlong UTF-8 shape: strict validation
|
||||
# must still convert them
|
||||
"latin1ovl": (
|
||||
"text/html; charset=iso-8859-1",
|
||||
b"",
|
||||
"À¡x.pdf".encode("latin-1"),
|
||||
"À¡x.pdf",
|
||||
),
|
||||
# header wins over meta: latin-1 href only resolves if iso-8859-1 is kept
|
||||
"priority": (
|
||||
"text/html; charset=iso-8859-1",
|
||||
b'<meta charset="utf-8">',
|
||||
"nuée.pdf".encode("latin-1"),
|
||||
"nuée.pdf",
|
||||
),
|
||||
"preenc": ("text/html", b"", quote(CHARSET_CJK).encode(), CHARSET_CJK),
|
||||
"bom": ("text/html", b"", CHARSET_CJK.encode(), CHARSET_CJK),
|
||||
}
|
||||
|
||||
def route_charset(self):
|
||||
path = unquote(urlsplit(self.path).path)
|
||||
parts = path.split("/")
|
||||
if path == "/charset/index.html":
|
||||
self.send_html(
|
||||
"".join(
|
||||
'\t<a href="%s/index.html">%s</a>\n' % (v, v)
|
||||
for v in self.CHARSET_VARIANTS
|
||||
)
|
||||
)
|
||||
return
|
||||
if len(parts) == 4 and parts[2] in self.CHARSET_VARIANTS:
|
||||
ctype, head, href, pdf = self.CHARSET_VARIANTS[parts[2]]
|
||||
if parts[3] == "index.html":
|
||||
body = (
|
||||
b"<html><head>"
|
||||
+ head
|
||||
+ b'</head><body><a href="'
|
||||
+ href
|
||||
+ b'">doc</a></body></html>'
|
||||
)
|
||||
if parts[2] == "bom":
|
||||
body = b"\xef\xbb\xbf" + body
|
||||
self.send_raw(body, ctype)
|
||||
return
|
||||
if parts[3] == pdf:
|
||||
self.send_raw(self.FAKE_PDF, "application/pdf")
|
||||
return
|
||||
self.send_response(404)
|
||||
self.send_header("Content-Length", "0")
|
||||
self.end_headers()
|
||||
|
||||
# resume / 416 loop (#206): the first GET stalls after a prefix so the crawl
|
||||
# can be interrupted (partial + temp-ref); every later request is 416.
|
||||
RESUME_PREFIX = b"PARTIAL-" + b"x" * 4096 # flushed before the stall
|
||||
@@ -1194,6 +1285,9 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
if path.startswith("/big/"):
|
||||
self.route_big()
|
||||
return True
|
||||
if path.startswith("/charset/"):
|
||||
self.route_charset()
|
||||
return True
|
||||
# Match percent-encoded paths (accented #157 route) by their decoded form.
|
||||
handler = self.ROUTES.get(path) or self.ROUTES.get(unquote(path))
|
||||
if handler is not None:
|
||||
|
||||
Reference in New Issue
Block a user