Compare commits

..

1 Commits

Author SHA1 Message Date
Xavier Roche
4020cda340 Remove the pre-3.31 (.dat/.ndx) cache import
The zip cache replaced this format in 3.31 (2003); what remained was an
import-only parser for hostile input, compiled unconditionally, plus a
tail of dead code keyed to it: the CACHE-1.5 writer behind if(1), the
cross-session HTML-MD5 dedup (its store was never written in the zip
era), the legacy legs of the startup rotation (which silently renamed
or deleted a user's .dat/.ndx pair) and of hts_cache_reconcile.

cache_init now detects a legacy pair, logs a clear refusal, leaves the
files untouched, and re-crawls. The cache-legacy self-test pins refusal
for both layouts (--update old.* and ro new.*), no is_update flag, and
untouched files; proven end-to-end against the local server. The
cacheindex self-test and fuzz-cachendx stay: cache_brstr/cache_binput
still back the -#C cache listing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-09 21:45:34 +02:00
16 changed files with 133 additions and 367 deletions

View File

@@ -43,12 +43,6 @@ jobs:
languages: c-cpp languages: c-cpp
build-mode: manual build-mode: manual
queries: security-extended 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. # Manual build: CodeQL traces the compiler, so build exactly what ships.
- name: Build - name: Build

View File

@@ -36,7 +36,6 @@ EXTRA_DIST = README.md run-fuzzers.sh \
corpus/unescape/percent.txt \ corpus/unescape/percent.txt \
corpus/filters/filter.bin corpus/filters/filter-size.bin \ corpus/filters/filter.bin corpus/filters/filter-size.bin \
corpus/filters/regress-empty-subject-unique.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/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/header/full-response.txt corpus/header/redirect.txt \

View File

@@ -50,13 +50,9 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
while (p != NULL && *p != '\0') { while (p != NULL && *p != '\0') {
char *nl = strchr(p, '\n'); char *nl = strchr(p, '\n');
size_t n = (nl != NULL) ? (size_t) (nl - p) : strlen(p); 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 */ memcpy(line, p, n);
for (i = 0; i < n; i++) line[n] = '\0';
if (p[i] != '\r')
line[len++] = p[i];
line[len] = '\0';
if (first) { if (first) {
treatfirstline(&r, line); treatfirstline(&r, line);
first = 0; first = 0;

View File

@@ -2,8 +2,8 @@
# Drive every built harness against its seed corpus. # 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> check deterministic replay (CI smoke)
# run-fuzzers.sh <build-fuzz-dir> [seconds] timed mutation run (discovery) # run-fuzzers.sh <build-fuzz-dir> [seconds] timed mutation run (discovery)
# Replay is crash/leak-only and never mutates; the per-unit -timeout guards # Replay is crash/leak-only and never mutates, so it can't hit strjoker's
# both modes against pathological slowdowns (e.g. pre-#501 strjoker). # catastrophic backtracking; the timed mode can, hence the per-unit -timeout.
set -euo pipefail set -euo pipefail
srcdir=$(cd "$(dirname "$0")" && pwd) srcdir=$(cd "$(dirname "$0")" && pwd)

View File

@@ -1646,104 +1646,138 @@ int back_add(struct_back * sback, httrackp * opt, cache_back * cache, const char
} }
} }
// tester cache // tester cache
if ((strcmp(adr, "file://")) /* pas fichier */ if ((strcmp(adr, "file://")) /* pas fichier */
&& ((!test) || &&((!test) || (cache->type == 1)) /* cache prioritaire, laisser passer en 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
((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; intptr_t hash_pos;
int hash_pos_return = 0; int hash_pos_return = 0;
#else
char *a = NULL;
#endif
#if HTS_FAST_CACHE
if (cache->hashtable) { if (cache->hashtable) {
#else
if (cache->use) {
#endif
char BIGSTK buff[HTS_URLMAXSIZE * 4]; char BIGSTK buff[HTS_URLMAXSIZE * 4];
#if HTS_FAST_CACHE
strcpybuff(buff, adr); strcpybuff(buff, adr);
strcatbuff(buff, fil); strcatbuff(buff, fil);
hash_pos_return = coucal_read(cache->hashtable, buff, &hash_pos); 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 // negative values when data is not in cache
if (hash_pos_return < 0) { if (hash_pos_return < 0) {
if (!test) { // not test mode #else
/* note: no check with IS_DELAYED_EXT() enabled - postcheck by if (a) {
* client please! */ #endif
if (save[0] != '\0' && !IS_DELAYED_EXT(save) && if (!test) { // non mode test
fsize_utf8(fconv(catbuff, sizeof(catbuff), save)) <= #if HTS_FAST_CACHE==0
0) { // final file missing or empty int pos = -1;
int found = 0;
/* It is possible that the file has been moved due to changes in a += strlen(buff);
* build structure */ sscanf(a, "%d", &pos); // lire position
{ #endif
char BIGSTK previous_save[HTS_URLMAXSIZE * 2];
htsblk r;
previous_save[0] = '\0'; #if HTS_FAST_CACHE==0
r = cache_readex(opt, cache, adr, fil, /*head */ NULL, 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,
/*bound to back[p] (temporary) */ /*bound to back[p] (temporary) */
back[p].location_buffer, previous_save, /*ro */ back[p].location_buffer, previous_save, /*ro */
1); 1);
/* Is supposed to be on disk only */ /* Is supposed to be on disk only */
if (r.is_write && previous_save[0] != '\0') { if (r.is_write && previous_save[0] != '\0') {
/* Exists, but with another (old) filename: rename (almost) /* Exists, but with another (old) filename: rename (almost) silently */
* silently */ if (strcmp(previous_save, save) != 0
if (strcmp(previous_save, save) != 0 && && fexist_utf8(fconv(catbuff, sizeof(catbuff), previous_save))) {
fexist_utf8( rename(fconv(catbuff, sizeof(catbuff), previous_save),
fconv(catbuff, sizeof(catbuff), previous_save))) { fconv(catbuff2, sizeof(catbuff2), save));
rename(fconv(catbuff, sizeof(catbuff), previous_save), if (fexist_utf8(fconv(catbuff, sizeof(catbuff), save))) {
fconv(catbuff2, sizeof(catbuff2), save)); found = 1;
if (fexist_utf8(fconv(catbuff, sizeof(catbuff), save))) { hts_log_print(opt, LOG_DEBUG,
found = 1; "File '%s' has been renamed since last mirror to '%s' ; applying changes",
hts_log_print(opt, LOG_DEBUG, previous_save, save);
"File '%s' has been renamed since last " } else {
"mirror to '%s' ; applying changes", hts_log_print(opt, LOG_ERROR,
previous_save, save); "Could not rename '%s' to '%s' ; will have to retransfer it",
} else { previous_save, save);
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 ? */ /* Not found ? */
if (!found) { if (!found) {
// invalidate: gone from disk, force a refetch #if HTS_FAST_CACHE
hash_pos_return = 0; hash_pos_return = 0;
if (opt->norecatch) { #else
if (!fexist_utf8(fconv( a = NULL;
catbuff, sizeof(catbuff), #endif
save))) { // declared but missing: user erased it // dévalider car non présent sur disque dans structure originale!!!
FILE *fp = // sinon, le fichier est ok à priori, mais on renverra un if-modified-since pour
FOPEN(fconv(catbuff, sizeof(catbuff), save), "wb"); // 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");
if (fp) if (fp)
fclose(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 {
hts_log_print(opt, LOG_WARNING, hts_log_print(opt, LOG_WARNING,
"Previous file '%s' not found (erased by " "Previous file '%s' not found (erased by user ?), recatching: %s%s",
"user ?), ignoring: %s%s",
save, back[p].url_adr, back[p].url_fil); 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
} // fsize() <= 0 #if HTS_FAST_CACHE==0
}
#endif
} }
} }
// //
} else { } else {
#if HTS_FAST_CACHE
hash_pos_return = 0; hash_pos_return = 0;
#else
a = NULL;
#endif
} }
if (hash_pos_return) { // in cache, with data // 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
const int cache_is_prioritary = cache->type == 1 const int cache_is_prioritary = cache->type == 1
|| opt->state.stop != 0; || opt->state.stop != 0;
if (cache_is_prioritary) { // cache prioritaire (pas de test if-modified..) if (cache_is_prioritary) { // cache prioritaire (pas de test if-modified..)
@@ -1864,8 +1898,7 @@ int back_add(struct_back * sback, httrackp * opt, cache_back * cache, const char
#endif #endif
} }
} }
/* Not in cache ; maybe in temporary cache ? Warning: non-movable /* Not in cache ; maybe in temporary cache ? Warning: non-movable "url_sav" */
"url_sav" */
else if (back_unserialize_ref(opt, adr, fil, &itemback) == 0) { else if (back_unserialize_ref(opt, adr, fil, &itemback) == 0) {
const off_t file_size = fsize_utf8(itemback->url_sav); const off_t file_size = fsize_utf8(itemback->url_sav);

View File

@@ -307,25 +307,14 @@ int cookie_load(t_cookie * cookie, const char *fpath, const char *name) {
return -1; return -1;
} }
/* Write cookies.txt; returns 0 on success. The jar holds live session // écrire cookies.txt
cookies, so keep it owner-only on Unix (Windows inherits folder ACLs). */ // !=0 : erreur
int cookie_save(t_cookie * cookie, const char *name) { int cookie_save(t_cookie * cookie, const char *name) {
char catbuff[CATBUFF_SIZE]; char catbuff[CATBUFF_SIZE];
if (strnotempty(cookie->data)) { if (strnotempty(cookie->data)) {
char BIGSTK line[8192]; char BIGSTK line[8192];
#ifdef _WIN32
FILE *fp = fopen(fconv(catbuff, sizeof(catbuff), name), "wb"); 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) { if (fp) {
char *a = cookie->data; char *a = cookie->data;

View File

@@ -491,7 +491,11 @@ static htsblk cache_readex_new(httrackp * opt, cache_back * cache,
hash_pos_return = coucal_read(cache->hashtable, buff, &hash_pos); hash_pos_return = coucal_read(cache->hashtable, buff, &hash_pos);
/* avoid errors on data entries */ /* avoid errors on data entries */
if (adr[0] == '/' && adr[1] == '/' && adr[2] == '[') { if (adr[0] == '/' && adr[1] == '/' && adr[2] == '[') {
#if HTS_FAST_CACHE
hash_pos_return = 0; hash_pos_return = 0;
#else
a = NULL;
#endif
} }
if (hash_pos_return != 0) { if (hash_pos_return != 0) {

View File

@@ -76,6 +76,9 @@ Please visit our Website: http://www.httrack.com
// always direct-to-disk (0/1) // always direct-to-disk (0/1)
#define HTS_DIRECTDISK_ALWAYS 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 // le > peut être considéré comme un tag de fermeture de commentaire (<!-- > est
// valide) // valide)
#define GT_ENDS_COMMENT 1 #define GT_ENDS_COMMENT 1

View File

@@ -134,6 +134,10 @@ RUN_CALLBACK0(opt, end); \
back_delete_all(opt, &cache, sback); \ back_delete_all(opt, &cache, sback); \
back_free(&sback); \ back_free(&sback); \
checkrobots_free(&robots); \ checkrobots_free(&robots); \
if (cache.use) { \
freet(cache.use); \
cache.use = NULL; \
} \
if (cache.zipOutput) { \ if (cache.zipOutput) { \
zipClose(cache.zipOutput, \ zipClose(cache.zipOutput, \
"Created by HTTrack Website Copier/" HTTRACK_VERSION); \ "Created by HTTrack Website Copier/" HTTRACK_VERSION); \

View File

@@ -194,6 +194,7 @@ struct cache_back {
int type; int type;
int ro; /**< read-only: no new cache is written */ int ro; /**< read-only: no new cache is written */
char *use; /**< in-memory list of cached adr+fil keys */
FILE *lst; /**< file list, used for purge */ FILE *lst; /**< file list, used for purge */
FILE *txt; /**< human-readable file list (info) */ FILE *txt; /**< human-readable file list (info) */
char lastmodified[256]; char lastmodified[256];

View File

@@ -48,7 +48,6 @@ Please visit our Website: http://www.httrack.com
#include "htsbase.h" #include "htsbase.h"
#include "htslib.h" #include "htslib.h"
#include <ctype.h> #include <ctype.h>
#include <stdint.h>
/* END specific definitions */ /* END specific definitions */
// à partir d'un tableau de {"+*.toto","-*.zip","+*.tata"} définit si nom est autorisé // à partir d'un tableau de {"+*.toto","-*.zip","+*.tata"} définit si nom est autorisé
@@ -121,81 +120,16 @@ int fa_strjoker_dual(int type, char **filters, int nfil, const char *nom1,
return use1 ? jok1 : jok2; return use1 ? jok1 : jok2;
} }
/* Failure memo for the recursive matcher: one bit per (chaine, joker) offset // supercomparateur joker (tm)
pair keeps star-heavy patterns polynomial instead of exponential (#501). */ // compare a et b (b=avec joker dedans), case insensitive [voir CI]
typedef struct strjoker_memo { // renvoi l'adresse de la première lettre de la chaine
const char *chaine0, *joker0; /* offsets are relative to these bases */ // (càd *[..]toto.. renvoi adresse de toto dans la chaine)
size_t stride; /* strlen(joker0) + 1 */ // accepte les délires du genre www.*.*/ * / * truc*.*
unsigned char *failed; /* failed-pair bitmap; NULL: no memo */ // cet algo est 'un peu' récursif mais ne consomme pas trop de tm
} strjoker_memo; // * = toute lettre
// --?-- : spécifique à HTTrack et aux ?
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, HTS_INLINE const char *strjoker(const char *chaine, const char *joker,
LLint *size, int *size_flag) { 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(joker) == 0) { // fin de chaine joker
if (strnotempty(chaine) == 0) // fin aussi pour la chaine: ok if (strnotempty(chaine) == 0) // fin aussi pour la chaine: ok
return chaine; return chaine;
@@ -369,7 +303,7 @@ static const char *strjoker_impl(const strjoker_memo *memo, const char *chaine,
// tester sans le joker (pas ()+ mais ()*) // tester sans le joker (pas ()+ mais ()*)
if (!unique) { if (!unique) {
if ((adr = strjoker_rec(memo, chaine, joker + jmp, size, size_flag))) { if ((adr = strjoker(chaine, joker + jmp, size, size_flag))) {
return adr; return adr;
} }
} }
@@ -381,8 +315,7 @@ static const char *strjoker_impl(const strjoker_memo *memo, const char *chaine,
max = strnotempty(chaine) ? 1 : 0; /* empty chaine: no char to eat */ max = strnotempty(chaine) ? 1 : 0; /* empty chaine: no char to eat */
while(i < (int) max) { while(i < (int) max) {
if (pass[(int) (unsigned char) chaine[i]]) { // caractère autorisé if (pass[(int) (unsigned char) chaine[i]]) { // caractère autorisé
if ((adr = strjoker_rec(memo, chaine + i + 1, joker + jmp, size, if ((adr = strjoker(chaine + i + 1, joker + jmp, size, size_flag))) {
size_flag))) {
return adr; return adr;
} }
i++; i++;
@@ -392,8 +325,7 @@ static const char *strjoker_impl(const strjoker_memo *memo, const char *chaine,
// tester chaîne vide // tester chaîne vide
if (i != max + 2) // avant c'est ok if (i != max + 2) // avant c'est ok
if ((adr = strjoker_rec(memo, chaine + max, joker + jmp, size, if ((adr = strjoker(chaine + max, joker + jmp, size, size_flag)))
size_flag)))
return adr; return adr;
return NULL; // perdu return NULL; // perdu
@@ -415,7 +347,7 @@ static const char *strjoker_impl(const strjoker_memo *memo, const char *chaine,
// comparaison ok? // comparaison ok?
if (ok) { if (ok) {
// continuer la comparaison. // continuer la comparaison.
if (strjoker_rec(memo, chaine + jmp, joker + jmp, size, size_flag)) if (strjoker(chaine + jmp, joker + jmp, size, size_flag))
return chaine; // retourner 1e lettre return chaine; // retourner 1e lettre
} }

View File

@@ -48,10 +48,6 @@ int fa_strjoker_dual(int type, char **filters, int nfil, const char *nom1,
const char *nom2, LLint *size, int *size_flag, int *depth); const char *nom2, LLint *size, int *size_flag, int *depth);
HTS_INLINE const char *strjoker(const char *chaine, const char *joker, LLint * size, HTS_INLINE const char *strjoker(const char *chaine, const char *joker, LLint * size,
int *size_flag); 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); const char *strjokerfind(const char *chaine, const char *joker);
#endif #endif

View File

@@ -422,10 +422,9 @@ typedef int T_SOC;
#endif #endif
/* Permission bits for created folders and files (mkdir and chmod). /* Permission bits for created folders and files (mkdir and chmod).
PROTECT_FOLDER/FILE are owner-only. With HTS_ACCESS set (the default) the PROTECT_FOLDER is owner-only. With HTS_ACCESS set (the default) the ACCESS_
ACCESS_ modes also grant group/other read; otherwise they stay owner-only. */ modes also grant group/other read; otherwise they stay owner-only. */
#define HTS_PROTECT_FOLDER (S_IRUSR | S_IWUSR | S_IXUSR) #define HTS_PROTECT_FOLDER (S_IRUSR | S_IWUSR | S_IXUSR)
#define HTS_PROTECT_FILE (S_IRUSR | S_IWUSR)
#if HTS_ACCESS #if HTS_ACCESS
#define HTS_ACCESS_FILE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) #define HTS_ACCESS_FILE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)

View File

@@ -621,105 +621,6 @@ static int st_filtersize(httrackp *opt, int argc, char **argv) {
return 0; 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). */ /* Merged two-form filter verdict via fa_strjoker_dual (see htsfilters.h). */
static int st_filterdual(httrackp *opt, int argc, char **argv) { static int st_filterdual(httrackp *opt, int argc, char **argv) {
int depth = -1, verdict; int depth = -1, verdict;
@@ -1662,26 +1563,6 @@ static int st_cookies(httrackp *opt, int argc, char **argv) {
err = 1; err = 1;
if (strstr(hdr, "junk") != NULL) // wrong-domain cookie leaked if (strstr(hdr, "junk") != NULL) // wrong-domain cookie leaked
err = 1; 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"); printf("cookie-header: %s\n", err ? "FAIL" : "OK");
if (err) if (err)
printf(" got: %s\n", hdr); printf(" got: %s\n", hdr);
@@ -2378,10 +2259,6 @@ static const struct selftest_entry {
{"filtersize", "<size> <string> <filter>...", {"filtersize", "<size> <string> <filter>...",
"size-aware filter verdict (negative size = unknown/scan time)", "size-aware filter verdict (negative size = unknown/scan time)",
st_filtersize}, 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>...", {"filterdual", "<string1> <string2> <filter>...",
"merged two-form filter verdict (fa_strjoker_dual)", st_filterdual}, "merged two-form filter verdict (fa_strjoker_dual)", st_filterdual},
{"simplify", "<path>", "collapse ./ and ../ in a path", st_simplify}, {"simplify", "<path>", "collapse ./ and ../ in a path", st_simplify},

View File

@@ -131,64 +131,3 @@ match '*.aspx' 'page.aspx?y=2'
# empty subject against a unique-match allow-all pattern (heap over-read regress) # empty subject against a unique-match allow-all pattern (heap over-read regress)
nomatch_empty '*((' nomatch_empty '*(('
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'