Compare commits

...

3 Commits

Author SHA1 Message Date
Xavier Roche
41dfcb8168 Size the wizard's filter slot instead of clipping the pattern
Clipping a filter pattern widens it: answer 7 built at a length that
overflows the slot lost its "*[file]" suffix down to a bare "*", turning
"the files in this directory" into "everything below it", recorded as an
allow rule from lengths the remote side chooses. So HTS_FILTER_SLOT_SIZE
now comes from the maxima the pattern is cut from (sign, host, separator,
path, suffix) rather than from HTS_URLMAXSIZE * 2, and filters_init()
allocates that same stride. Nothing is truncated, so nothing aborts
either, which is what #1264 asked for.

Signed-off-by: Xavier Roche <xroche@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-08-14 18:35:06 +02:00
Xavier Roche
2d714d073a Merge remote-tracking branch 'origin/master' into wizard-filter-clip-1264 2026-08-14 18:10:38 +02:00
Xavier Roche
08d469eeb3 A long URL aborts the run when the wizard applies an answer
hts_wizard_answer_filter() builds the filter pattern out of the link the
question is about, into a buffer the size of one filter slot, and htsbuff
aborts on overflow. Answer 7 emits sign, host, path and "*[file]", which is
eight bytes more than the "http://" + host + path form the wizard already
built, so a link that fits everywhere else kills the crawl here.

Clip instead, per AGENTS.md: only the tail is lost, so the filter stays
anchored on the link it was cut from.

Closes #1264

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <xroche@gmail.com>
2026-08-14 14:08:37 +02:00
5 changed files with 134 additions and 18 deletions

View File

@@ -698,7 +698,7 @@ int httpmirror(char *url1, httrackp * opt) {
}
}
{
htsbuff fb = htsbuff_ptr(filters[filptr], HTS_URLMAXSIZE * 2);
htsbuff fb = htsbuff_ptr(filters[filptr], HTS_FILTER_SLOT_SIZE);
htsbuff_cpy(&fb, type ? "+" : "-");
htsbuff_cat(&fb, tempo);
}
@@ -2348,7 +2348,7 @@ void host_ban(httrackp * opt, int ptr,
// interdire host
assertf((*_FILTERS_PTR) < opt->maxfilter);
if (*_FILTERS_PTR < opt->maxfilter) {
htsbuff fb = htsbuff_ptr(_FILTERS[*_FILTERS_PTR], HTS_URLMAXSIZE * 2);
htsbuff fb = htsbuff_ptr(_FILTERS[*_FILTERS_PTR], HTS_FILTER_SLOT_SIZE);
htsbuff_cpy(&fb, "-");
htsbuff_cat(&fb, host);
htsbuff_cat(&fb, "/*"); // forbid host/*
@@ -2438,16 +2438,13 @@ int filters_init(char ***ptrfilters, int maxfilter, int filterinc) {
}
if (filters) {
if (filters[0] == NULL) {
filters[0] =
(char *) malloct(sizeof(char) * (filter_max + 2) *
(HTS_URLMAXSIZE * 2));
filters[0] = (char *) malloct(sizeof(char) * (filter_max + 2) *
HTS_FILTER_SLOT_SIZE);
memset(filters[0], 0,
sizeof(char) * (filter_max + 2) * (HTS_URLMAXSIZE * 2));
sizeof(char) * (filter_max + 2) * HTS_FILTER_SLOT_SIZE);
} else {
filters[0] =
(char *) realloct(filters[0],
sizeof(char) * (filter_max +
2) * (HTS_URLMAXSIZE * 2));
filters[0] = (char *) realloct(
filters[0], sizeof(char) * (filter_max + 2) * HTS_FILTER_SLOT_SIZE);
}
if (filters[0] == NULL) {
freet(filters);
@@ -2463,7 +2460,7 @@ int filters_init(char ***ptrfilters, int maxfilter, int filterinc) {
else
from = filter_max - filterinc;
for(i = 0; i <= filter_max; i++) { // PLUS UN (sécurité)
filters[i] = filters[0] + i * (HTS_URLMAXSIZE * 2);
filters[i] = filters[0] + i * HTS_FILTER_SLOT_SIZE;
}
for(i = from; i <= filter_max; i++) { // PLUS UN (sécurité)
filters[i][0] = '\0'; // clear

View File

@@ -4132,10 +4132,62 @@ static int st_hashkey_bounds(httrackp *opt, int argc, char **argv) {
return 0;
}
/* The pattern answer n owes (adr, fil): a second implementation of the builder,
with no bound of its own, to compare it against. */
static void wizardfilter_want(htsbuff *w, int n, const char *adr,
const char *fil, hts_boolean up) {
htsbuff_cpy(w, n <= 2 ? "-" : "+");
htsbuff_cat(w, adr);
if (n == 2 || n == 6 || (n == 5 && up)) {
htsbuff_cat(w, "/*");
return;
}
if (*fil != '/')
htsbuff_cat(w, "/");
htsbuff_cat(w, fil);
if (n == 1 || n == 5)
htsbuff_cat(w, "*");
else if (n == 7)
htsbuff_cat(w, HTS_WIZARD_FILTER_SUFFIX);
}
/* A link of the given lengths: adr all 'a', fil a directory of 'b' ending on a
slash for answers 1, 5 and 7 to anchor on, led by one when `slash`. */
static void wizardfilter_link(char *adr, size_t adrlen, char *fil,
size_t fillen, hts_boolean slash) {
memset(adr, 'a', adrlen);
adr[adrlen] = '\0';
memset(fil, 'b', fillen);
fil[0] = slash ? '/' : 'b';
fil[fillen - 1] = '/';
fil[fillen] = '\0';
}
/* Asserts every answer that emits a filter for (adr, fil) emits exactly what
wizardfilter_want() owes, and returns the longest pattern seen. */
static size_t wizardfilter_emits(htsbuff *f, char *expect, const char *adr,
const char *fil) {
static const int answers[] = {0, 1, 2, 5, 6, 7};
size_t i, up, longest = 0;
for (i = 0; i < sizeof(answers) / sizeof(answers[0]); i++) {
for (up = 0; up < 2; up++) {
const hts_boolean seek = up != 0 ? HTS_TRUE : HTS_FALSE;
htsbuff w = htsbuff_ptr(expect, HTS_FILTER_SLOT_SIZE);
wizardfilter_want(&w, answers[i], adr, fil, seek);
hts_wizard_answer_filter(f, 0, answers[i], adr, fil, seek);
assertf(strcmp(f->buf, expect) == 0);
longest = f->len > longest ? f->len : longest;
}
}
return longest;
}
/* Prints the filter answer <n> emits for (adr, fil) [up] in [slot]; with no
arguments, asserts every answer against its expected pattern (#1119). */
static int st_wizardfilter(httrackp *opt, int argc, char **argv) {
char pattern[HTS_URLMAXSIZE * 2];
char pattern[HTS_FILTER_SLOT_SIZE];
htsbuff f = htsbuff_array(pattern);
(void) opt;
@@ -4188,6 +4240,33 @@ static int st_wizardfilter(httrackp *opt, int argc, char **argv) {
EMITS(5, "foo.com", "page.html", HTS_FALSE, "");
EMITS(7, "foo.com", "page.html", HTS_FALSE, "");
/* A long link must emit the same pattern a short one does, byte for byte:
clipping it would widen the rule (a cut "*[file]" becomes "*"), and the
lengths come from the wire. Sweeps the old 2048-byte slot, then the real
worst case, both link buffers full. */
{
const size_t urlmax = HTS_URLMAXSIZE * 2 - 1; /* the engine's adr and fil */
char *adr = malloct(urlmax + 1);
char *fil = malloct(urlmax + 1);
char *expect = malloct(HTS_FILTER_SLOT_SIZE);
size_t total;
for (total = 2040; total <= 2048; total++) {
const size_t adrlen = total / 2, fillen = total - 1 - adrlen;
wizardfilter_link(adr, adrlen, fil, fillen, HTS_TRUE);
wizardfilter_emits(&f, expect, adr, fil);
}
/* the real maximum: both buffers full, and no leading slash on fil to
spare the separator. It fills the slot exactly, NUL included */
wizardfilter_link(adr, urlmax, fil, urlmax, HTS_FALSE);
assertf(wizardfilter_emits(&f, expect, adr, fil) ==
HTS_FILTER_SLOT_SIZE - 1);
freet(adr);
freet(fil);
freet(expect);
}
/* the answers that add no filter at all */
EMITS(-1, "foo.com", "/x", HTS_FALSE, "");
EMITS(3, "foo.com", "/x", HTS_FALSE, "");

View File

@@ -43,10 +43,6 @@ Please visit our Website: http://www.httrack.com
/* END specific definitions */
// libérer filters[0] pour insérer un élément dans filters[0]
/* Per-slot capacity of the filters array, matching the slot stride allocated by
filters_init() in htscore.c (HTS_URLMAXSIZE * 2). */
#define HTS_FILTER_SLOT_SIZE (HTS_URLMAXSIZE * 2)
#define HT_INSERT_FILTERS0 \
do { \
int i; \
@@ -265,6 +261,8 @@ void hts_wizard_answer_filter(htsbuff *f, int slot, int n, const char *adr,
while (fil[dir] != '/' && dir > 0)
dir--;
/* a clipped pattern would be a wider rule than the answer asked for */
assertf(f->cap >= HTS_FILTER_SLOT_SIZE);
htsbuff_cpy(f, "");
if (hts_wizard_scope_answer(n) != HTS_DEFAULT) {
wizard_cat_scope(f, hts_wizard_scope_answer(n) == HTS_TRUE ? "-" : "+", adr,
@@ -313,7 +311,7 @@ void hts_wizard_answer_filter(htsbuff *f, int slot, int n, const char *adr,
case 7: /* this directory, files only */
if (fil[dir] == '/') {
wizard_cat_path(f, "+", adr, fil, dir + 1);
htsbuff_cat(f, "*[file]");
htsbuff_cat(f, HTS_WIZARD_FILTER_SUFFIX);
}
break;

View File

@@ -61,10 +61,22 @@ hts_boolean hts_robots_forbids(httrackp *opt, const char *adr, const char *fil,
caller stops at the first empty one. */
#define HTS_WIZARD_MAX_FILTERS 2
/* The longest suffix an answer appends after the link (answer 7). */
#define HTS_WIZARD_FILTER_SUFFIX "*[file]"
/* Capacity of one filters[] slot, and the stride filters_init() allocates in
htscore.c: change the two together. Sized for the longest pattern an answer
can build (sign, host, separator, path, suffix), because clipping one widens
it: "*[file]" cut back to "*" allows every file below, not just this one. */
#define HTS_FILTER_SLOT_SIZE \
(1 + (HTS_URLMAXSIZE * 2 - 1) + 1 + (HTS_URLMAXSIZE * 2 - 1) + \
sizeof(HTS_WIZARD_FILTER_SUFFIX))
/* Builds into `f` the `slot`-th filter answer `n` adds for the link (adr,fil),
and leaves `f` empty past the last one. Only the host-scope answers emit a
second, because their starred form misses the apex. `seeker_up` is the
HTS_SEEKER_UP bit of opt->seeker, read by answer 5. */
HTS_SEEKER_UP bit of opt->seeker, read by answer 5. `f` must have a full
HTS_FILTER_SLOT_SIZE of capacity, so no pattern is ever truncated. */
void hts_wizard_answer_filter(htsbuff *f, int slot, int n, const char *adr,
const char *fil, hts_boolean seeker_up);

View File

@@ -0,0 +1,30 @@
#!/bin/bash
#
set -euo pipefail
# shellcheck source=tests/testlib.sh
. "$(dirname "$0")/testlib.sh"
# #1264/#1266: the pattern is cut from the link the question is about, so a long
# enough one used to outgrow the filter slot. It must neither abort the run nor
# clip: a clipped pattern is a wider rule than the one the answer asked for.
# each CLI argument stays under HTS_CDLMAXSIZE, so the length comes from both
adr=$(printf 'a%.0s' $(seq 1 1023))
# 1 + len(adr) + len(fil), sweeping the 2048 bytes the slot used to hold
for total in $(seq 2040 2047); do
fil=/$(printf 'b%.0s' $(seq 1 $((total - ${#adr} - 3))))/
test $((1 + ${#adr} + ${#fil})) -eq "$total" || fail "bad length for $total"
assert_selftest "-$adr$fil" wizardfilter 0 "$adr" "$fil"
assert_selftest "-$adr$fil*" wizardfilter 1 "$adr" "$fil"
assert_selftest "-$adr/*" wizardfilter 2 "$adr" "$fil"
assert_selftest "+$adr$fil*" wizardfilter 5 "$adr" "$fil"
assert_selftest "+$adr/*" wizardfilter 6 "$adr" "$fil"
assert_selftest "+$adr$fil*[file]" wizardfilter 7 "$adr" "$fil"
done
# a command line cannot express the rest of the sweep, up to both link buffers
# full; the self-test carries it
assert_selftest "wizardfilter self-test OK" wizardfilter