Compare commits

..

2 Commits

Author SHA1 Message Date
Xavier Roche
3ab3726508 Keep the vcxproj source list alphabetical
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-14 16:11:38 +02:00
Xavier Roche
573e62e08c Extract the proxy tunneling code into htsproxy.c
htslib.c is the engine's catch-all "subroutines" file; the two proxy
tunnels living in it (HTTP CONNECT and the SOCKS5 handshake) are one
coherent thing: blocking raw-socket negotiation with the proxy, run from
the same back_wait callsite before TLS. Move both, plus proxy_getline and
the socks5 statics, into htsproxy.c/.h, which also gives the socks5_test_io
self-test seam a home away from the public-ish htslib.h.

Pure code motion: the moved bytes are identical, and hts_proxy_is_socks
stays in htslib.c next to hts_parse_proxy and jump_protocol_const, being a
URL-scheme predicate rather than handshake logic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-14 15:47:24 +02:00
29 changed files with 241 additions and 619 deletions

5
.gitattributes vendored
View File

@@ -1,8 +1,3 @@
# Resource scripts are Windows tooling input and must stay CRLF: the engine has no # Resource scripts are Windows tooling input and must stay CRLF: the engine has no
# encoding guard, so an autocrlf checkout could otherwise flatten them silently. # encoding guard, so an autocrlf checkout could otherwise flatten them silently.
*.rc text eol=crlf *.rc text eol=crlf
# The test scripts run under Git Bash on the Windows CI; a CRLF checkout makes
# bash die on $'\r' on every line of them.
*.test text eol=lf
*.sh text eol=lf

View File

@@ -119,61 +119,22 @@ jobs:
throw "CRT mismatch across the boundary: libhttrack.dll=$a httrack.exe=$b" throw "CRT mismatch across the boundary: libhttrack.dll=$a httrack.exe=$b"
} }
# The engine ships ~90 tests and none had ever run on Windows: "make check" # The engine self-tests exercise the codecs the DLL was linked with: a
# is Linux/macOS only. These are the offline ones, driven from Git Bash # missing brotli/zstd decoder shows up here, not just at link time.
# against the native httrack.exe. They subsume the self-tests this step - name: Run the content-coding self-tests
# used to run inline (codecs, cache, fsize). shell: pwsh
- name: Run the engine test suite (offline tests)
shell: bash
working-directory: tests
run: | run: |
set -u $ErrorActionPreference = "Stop"
bin="$(cygpath -u "$GITHUB_WORKSPACE")/src/${{ matrix.platform }}/${{ matrix.configuration }}" $exe = "src\${{ matrix.platform }}\${{ matrix.configuration }}\httrack.exe"
export PATH="$bin:$PATH" $tmp = New-Item -ItemType Directory -Path (Join-Path $env:RUNNER_TEMP "st")
command -v httrack >/dev/null || { echo "::error::no httrack.exe in $bin"; exit 1; } foreach ($t in @("acceptencoding", "contentcodings")) {
$out = & $exe -O NUL "-#test=$t" $tmp.FullName run
# httrack.exe is native, so MSYS rewrites any argument shaped like a if ($LASTEXITCODE -ne 0) { throw "$t failed" }
# POSIX path, and a URL path is shaped exactly like one: "/a/b.html" if (-not ($out | Select-String -SimpleMatch "$t self-test OK")) {
# reached the engine as "C:/Program Files/Git/a/b.html". Switch that throw "$t did not report OK: $out"
# off, and hand the tests a TMPDIR that is already a Windows path. }
export MSYS_NO_PATHCONV=1 Write-Host ($out -join "`n")
export MSYS2_ARG_CONV_EXCL='*' }
TMPDIR="$(cygpath -m "$RUNNER_TEMP")"
export TMPDIR
pass=0 fail=0 skip=0 failed=""
for t in 00_runnable.test 01_engine-*.test 01_zlib-*.test; do
rc=0
bash "$t" >"$t.log" 2>&1 || rc=$?
case "$rc" in
0) pass=$((pass + 1)); echo "PASS $t" ;;
77) skip=$((skip + 1)); echo "SKIP $t" ;;
*)
fail=$((fail + 1)) failed="$failed $t"
echo "FAIL $t (exit $rc)"
# These assert with `test "$(...)" == "..." || exit 1`, which
# says nothing at all on failure. Re-run traced.
bash -x "$t" >>"$t.log" 2>&1 || true
tail -n 25 "$t.log" | sed 's/^/ /'
;;
esac
done
echo "ran=$((pass + fail + skip)) pass=$pass fail=$fail skip=$skip" |
tee -a "$GITHUB_STEP_SUMMARY"
# Every gate in these scripts exits 77, so a suite that degraded to
# all-skipped would report green having tested nothing: assert a floor
# on what actually ran, not just the absence of failures.
[ "$pass" -ge 45 ] || { echo "::error::only $pass tests passed ($skip skipped)"; exit 1; }
[ "$fail" -eq 0 ] || { echo "::error::failing:$failed"; exit 1; }
- name: Upload the test logs
if: always()
uses: actions/upload-artifact@v4
with:
name: engine-tests-${{ matrix.platform }}-${{ matrix.configuration }}
path: tests/*.log
if-no-files-found: warn
- name: Upload MSBuild logs - name: Upload MSBuild logs
if: always() if: always()

View File

@@ -140,9 +140,8 @@ int back_connect_fallback_due(int addr_index, int addr_count, int elapsed,
/* Pending-connect result for a non-blocking socket reported ready by select(): /* Pending-connect result for a non-blocking socket reported ready by select():
0 = connected, >0 = the connect errno (refused, unreachable, ...), -1 if the 0 = connected, >0 = the connect errno (refused, unreachable, ...), -1 if the
probe itself failed. A failed connect is reported ready as well (writable on probe itself failed. A failed connect is reported writable too, so this is
posix, exception set on winsock), so this is how success is told from failure how success is told from failure without blocking. */
without blocking. */
static int connect_socket_error(T_SOC soc) { static int connect_socket_error(T_SOC soc) {
int soerr = 0; int soerr = 0;
socklen_t len = (socklen_t) sizeof(soerr); socklen_t len = (socklen_t) sizeof(soerr);
@@ -777,7 +776,7 @@ int back_finalize(httrackp * opt, cache_back * cache, struct_back * sback,
if (back[p].r.statuscode == HTTP_OK) { // OK (ou 304 en backing) if (back[p].r.statuscode == HTTP_OK) { // OK (ou 304 en backing)
if (back[p].r.is_write) { // Written file if (back[p].r.is_write) { // Written file
if (may_be_hypertext_mime(opt, back[p].r.contenttype, back[p].url_fil)) { // to parse! if (may_be_hypertext_mime(opt, back[p].r.contenttype, back[p].url_fil)) { // to parse!
LLint sz; off_t sz;
sz = fsize_utf8(back[p].url_sav); sz = fsize_utf8(back[p].url_sav);
if (sz > 0) { // ok, exists! if (sz > 0) { // ok, exists!
@@ -1954,7 +1953,7 @@ int back_add(struct_back * sback, httrackp * opt, cache_back * cache, const char
/* 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 LLint file_size = fsize_utf8(itemback->url_sav); const off_t file_size = fsize_utf8(itemback->url_sav);
/* Found file on disk */ /* Found file on disk */
if (file_size > 0) { if (file_size > 0) {
@@ -1989,7 +1988,7 @@ int back_add(struct_back * sback, httrackp * opt, cache_back * cache, const char
} }
/* Not in cache or temporary cache ; found on disk ? (hack) */ /* Not in cache or temporary cache ; found on disk ? (hack) */
else if (fexist_utf8(save)) { else if (fexist_utf8(save)) {
const LLint sz = fsize_utf8(save); const off_t sz = fsize_utf8(save);
// Bon, là il est possible que le fichier ait été partiellement transféré // Bon, là il est possible que le fichier ait été partiellement transféré
// (s'il l'avait été en totalité il aurait été inscrit dans le cache ET existerait sur disque) // (s'il l'avait été en totalité il aurait été inscrit dans le cache ET existerait sur disque)
@@ -2659,9 +2658,7 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
for (i_mod = 0; i_mod < (unsigned int) back_max; i_mod++) { for (i_mod = 0; i_mod < (unsigned int) back_max; i_mod++) {
unsigned int i = (i_mod + mod_random) % (back_max); unsigned int i = (i_mod + mod_random) % (back_max);
// winsock flags a failed connect in the exception set only: leave a if (back[i].status > 0) {
// connecting slot to the connect handler, which can still fall back
if (back[i].status > 0 && back[i].status != STATUS_CONNECTING) {
if (!back[i].r.is_file) { // not file.. if (!back[i].r.is_file) { // not file..
if (back[i].r.soc != INVALID_SOCKET) { // hey, you never know.. if (back[i].r.soc != INVALID_SOCKET) { // hey, you never know..
int err = FD_ISSET(back[i].r.soc, &fds_e); int err = FD_ISSET(back[i].r.soc, &fds_e);
@@ -2675,7 +2672,10 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
} }
back[i].r.soc = INVALID_SOCKET; back[i].r.soc = INVALID_SOCKET;
back[i].r.statuscode = STATUSCODE_CONNERROR; back[i].r.statuscode = STATUSCODE_CONNERROR;
strcpybuff(back[i].r.msg, "Receive Error"); if (back[i].status == STATUS_CONNECTING)
strcpybuff(back[i].r.msg, "Connect Error");
else
strcpybuff(back[i].r.msg, "Receive Error");
if (back[i].status == STATUS_ALIVE) { /* Keep-alive socket */ if (back[i].status == STATUS_ALIVE) { /* Keep-alive socket */
back_delete(opt, cache, sback, i); back_delete(opt, cache, sback, i);
} else { } else {
@@ -2708,11 +2708,10 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
gestion_timeout = 1; gestion_timeout = 1;
// connecté? // connecté?
dispo = back[i].r.soc != INVALID_SOCKET && dispo = FD_ISSET(back[i].r.soc, &fds_c);
(FD_ISSET(back[i].r.soc, &fds_c) ||
FD_ISSET(back[i].r.soc, &fds_e));
if (dispo) { // socket ready: connect() finished (ok or failed) if (dispo) { // socket ready: connect() finished (ok or failed)
// probe SO_ERROR and, on failure, fall back to the next address // a refused/failed connect is reported writable too; probe SO_ERROR
// and, on failure, fall back to the next address (or fail the slot)
if (connect_socket_error(back[i].r.soc) != 0) { if (connect_socket_error(back[i].r.soc) != 0) {
if (!back_connect_next(opt, sback, i)) { if (!back_connect_next(opt, sback, i)) {
deletehttp(&back[i].r); deletehttp(&back[i].r);
@@ -3696,7 +3695,7 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
if (back[i].r.statuscode == HTTP_OK && !back[i].testmode) { // 'OK' if (back[i].r.statuscode == HTTP_OK && !back[i].testmode) { // 'OK'
if (!is_hypertext_mime(opt, back[i].r.contenttype, back[i].url_fil)) { // not HTML if (!is_hypertext_mime(opt, back[i].r.contenttype, back[i].url_fil)) { // not HTML
if (strnotempty(back[i].url_sav)) { // target found if (strnotempty(back[i].url_sav)) { // target found
LLint size = fsize_utf8(back[i].url_sav); off_t size = fsize_utf8(back[i].url_sav);
if (size >= 0) { if (size >= 0) {
if (back[i].r.totalsize == size) { // same size! if (back[i].r.totalsize == size) { // same size!
@@ -3915,7 +3914,7 @@ void back_wait(struct_back * sback, httrackp * opt, cache_back * cache,
// traiter 206 (partial content) // traiter 206 (partial content)
// xxc SI CHUNK VERIFIER QUE CA MARCHE?? // xxc SI CHUNK VERIFIER QUE CA MARCHE??
if (back[i].r.statuscode == 206) { // on nous envoie un morceau (la fin) coz une partie sur disque! if (back[i].r.statuscode == 206) { // on nous envoie un morceau (la fin) coz une partie sur disque!
LLint sz = fsize_utf8(back[i].url_sav); off_t sz = fsize_utf8(back[i].url_sav);
/* RFC 7233: resume at the server's Content-Range start, /* RFC 7233: resume at the server's Content-Range start,
not the offset we requested; a server may resume not the offset we requested; a server may resume
earlier and appending the overlap duplicates bytes earlier and appending the overlap duplicates bytes

View File

@@ -356,7 +356,7 @@ void cache_add(httrackp * opt, cache_back * cache, const htsblk * r,
FILE *fp; FILE *fp;
// On recopie le fichier->. // On recopie le fichier->.
LLint file_size = fsize_utf8(fconv(catbuff, sizeof(catbuff), url_save)); off_t file_size = fsize_utf8(fconv(catbuff, sizeof(catbuff), url_save));
if (file_size >= 0) { if (file_size >= 0) {
fp = FOPEN(fconv(catbuff, sizeof(catbuff), url_save), "rb"); fp = FOPEN(fconv(catbuff, sizeof(catbuff), url_save), "rb");
@@ -1241,27 +1241,23 @@ char *readfile(const char *fil) {
char *readfile2(const char *fil, LLint * size) { char *readfile2(const char *fil, LLint * size) {
char *adr = NULL; char *adr = NULL;
char catbuff[CATBUFF_SIZE]; char catbuff[CATBUFF_SIZE];
const LLint len = fsize(fil); INTsys len = 0;
/* a size too large for size_t (32-bit Windows/i386) must fail closed: it len = fsize(fil);
would wrap malloct() short while fread() still read the untruncated len */ if (len >= 0) { // exists
const size_t buflen = len >= 0 ? llint_to_size_t(len) : (size_t) -1;
if (buflen != (size_t) -1) { // exists, and is addressable
FILE *fp; FILE *fp;
fp = fopen(fconv(catbuff, sizeof(catbuff), fil), "rb"); fp = fopen(fconv(catbuff, sizeof(catbuff), fil), "rb");
if (fp != NULL) { // n'existe pas (!) if (fp != NULL) { // n'existe pas (!)
adr = (char *) malloct(buflen + 1); adr = (char *) malloct(len + 1);
if (size != NULL) if (size != NULL)
*size = len; *size = len;
if (adr != NULL) { if (adr != NULL) {
if (buflen > 0 && if (len > 0 && fread(adr, 1, len, fp) != len) { // fichier endommagé ?
fread(adr, 1, buflen, fp) != buflen) { // fichier endommagé ?
freet(adr); freet(adr);
adr = NULL; adr = NULL;
} else } else
*(adr + buflen) = '\0'; *(adr + len) = '\0';
} }
fclose(fp); fclose(fp);
} }
@@ -1273,21 +1269,19 @@ char *readfile2(const char *fil, LLint * size) {
char *readfile_utf8(const char *fil) { char *readfile_utf8(const char *fil) {
char *adr = NULL; char *adr = NULL;
char catbuff[CATBUFF_SIZE]; char catbuff[CATBUFF_SIZE];
const LLint len = fsize_utf8(fil); const off_t len = fsize_utf8(fil);
const size_t buflen = len >= 0 ? llint_to_size_t(len) : (size_t) -1;
if (buflen != (size_t) -1) { // exists, and is addressable (see readfile2) if (len >= 0) { // exists
FILE *const fp = FOPEN(fconv(catbuff, sizeof(catbuff), fil), "rb"); FILE *const fp = FOPEN(fconv(catbuff, sizeof(catbuff), fil), "rb");
if (fp != NULL) { // n'existe pas (!) if (fp != NULL) { // n'existe pas (!)
adr = (char *) malloct(buflen + 1); adr = (char *) malloct(len + 1);
if (adr != NULL) { if (adr != NULL) {
if (buflen > 0 && if (len > 0 && fread(adr, 1, len, fp) != len) { // fichier endommagé ?
fread(adr, 1, buflen, fp) != buflen) { // fichier endommagé ?
freet(adr); freet(adr);
adr = NULL; adr = NULL;
} else { } else {
adr[buflen] = '\0'; adr[len] = '\0';
} }
} }
fclose(fp); fclose(fp);

View File

@@ -938,9 +938,9 @@ static void reconcile_put(httrackp *opt, const char *name, size_t size) {
} }
/* Expect `name` to weigh `size` bytes, or be absent when size == -1. */ /* Expect `name` to weigh `size` bytes, or be absent when size == -1. */
static int reconcile_expect(httrackp *opt, const char *name, LLint size, static int reconcile_expect(httrackp *opt, const char *name, off_t size,
const char *what) { const char *what) {
const LLint got = fsize(reconcile_st_path(opt, name)); const off_t got = fsize(reconcile_st_path(opt, name));
if (got != size) { if (got != size) {
fprintf(stderr, "cache-reconcile: %s: %s is %d bytes, expected %d\n", what, fprintf(stderr, "cache-reconcile: %s: %s is %d bytes, expected %d\n", what,
@@ -954,7 +954,7 @@ int cache_reconcile_selftest(httrackp *opt, const char *dir) {
int failures = 0; int failures = 0;
/* around the interrupted-run thresholds (new < 32768, old > 65536) */ /* around the interrupted-run thresholds (new < 32768, old > 65536) */
static const LLint TINY = 1024, MID = 40000, SOLID = 131072; static const off_t TINY = 1024, MID = 40000, SOLID = 131072;
golden_setup(opt, dir); golden_setup(opt, dir);
#ifdef _WIN32 #ifdef _WIN32

View File

@@ -31,9 +31,6 @@ Please visit our Website: http://www.httrack.com
/* ------------------------------------------------------------ */ /* ------------------------------------------------------------ */
#include "htscharset.h" #include "htscharset.h"
#ifdef _WIN32
#include <shellapi.h>
#endif
#include "htsbase.h" #include "htsbase.h"
#include "punycode.h" #include "punycode.h"
#include "htssafe.h" #include "htssafe.h"
@@ -385,44 +382,10 @@ char *hts_convertStringFromUTF8(const char *s, size_t size, const char *charset)
return hts_convertStringCPFromUTF8(s, size, cp); return hts_convertStringCPFromUTF8(s, size, cp);
} }
HTSEXT_API char *hts_convertStringSystemToUTF8(const char *s, size_t size) { char *hts_convertStringSystemToUTF8(const char *s, size_t size) {
return hts_convertStringCPToUTF8(s, size, GetACP()); return hts_convertStringCPToUTF8(s, size, GetACP());
} }
HTSEXT_API void hts_argv_utf8(int *pargc, char ***pargv) {
int wargc = 0;
LPWSTR *const wargv = CommandLineToArgvW(GetCommandLineW(), &wargc);
char **argv;
int i;
// On any failure keep the CRT's ANSI argv: lossy, but never half-converted.
if (wargv == NULL)
return;
if (wargc <= 0 ||
(argv = calloct((size_t) wargc + 1, sizeof(char *))) == NULL) {
LocalFree(wargv);
return;
}
for (i = 0; i < wargc; i++) {
const int wsize = (int) wcslen(wargv[i]);
// hts_convertUCS2StringToUTF8() returns NULL on an empty string
argv[i] =
wsize != 0 ? hts_convertUCS2StringToUTF8(wargv[i], wsize) : strdupt("");
if (argv[i] == NULL) {
while (i-- != 0)
freet(argv[i]);
freet(argv);
LocalFree(wargv);
return;
}
}
argv[wargc] = NULL; // callers may rely on argv[argc] == NULL
LocalFree(wargv);
*pargc = wargc;
*pargv = argv; // never freed: argv lives for the process
}
#else #else
#include <errno.h> #include <errno.h>

View File

@@ -34,7 +34,6 @@ Please visit our Website: http://www.httrack.com
#define HTS_CHARSET_DEFH #define HTS_CHARSET_DEFH
/** Standard includes. **/ /** Standard includes. **/
#include "htsglobal.h"
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#ifdef _WIN32 #ifdef _WIN32
@@ -175,16 +174,7 @@ extern char *hts_convertUCS2StringToUTF8(LPWSTR woutput, int wsize);
* Convert current system codepage to UTF-8. * Convert current system codepage to UTF-8.
* This function is WIN32 specific. * This function is WIN32 specific.
**/ **/
HTSEXT_API char *hts_convertStringSystemToUTF8(const char *s, size_t size); extern char *hts_convertStringSystemToUTF8(const char *s, size_t size);
/**
* Replace the CRT's ANSI argv by a UTF-8 one decoded from the real UTF-16
* command line: every char* is UTF-8 on Windows (FOPEN, STAT, ... convert at
* the syscall boundary). Keeps the CRT's argv on failure; the new array is
* writable, NULL-terminated, and lives for the process.
* This function is WIN32 specific.
**/
HTSEXT_API void hts_argv_utf8(int *pargc, char ***pargv);
#endif #endif
#endif #endif

View File

@@ -855,7 +855,7 @@ int httpmirror(char *url1, httrackp * opt) {
char *filelist_buff = NULL; char *filelist_buff = NULL;
size_t filelist_sz = 0; size_t filelist_sz = 0;
const char *filelist_err = NULL; /* failure reason, NULL on success */ const char *filelist_err = NULL; /* failure reason, NULL on success */
const LLint fs = fsize(StringBuff(opt->filelist)); const off_t fs = fsize(StringBuff(opt->filelist));
if (fs < 0) { if (fs < 0) {
/* fsize() hides the cause; redo stat() for a precise errno (#49) */ /* fsize() hides the cause; redo stat() for a precise errno (#49) */
@@ -863,7 +863,7 @@ int httpmirror(char *url1, httrackp * opt) {
filelist_err = stat(StringBuff(opt->filelist), &st) != 0 filelist_err = stat(StringBuff(opt->filelist), &st) != 0
? strerror(errno) ? strerror(errno)
: "not a regular file"; : "not a regular file";
} else if ((filelist_sz = llint_to_size_t(fs)) == (size_t) -1) { } else if ((filelist_sz = off_t_to_size_t(fs)) == (size_t) -1) {
filelist_err = "file too large"; filelist_err = "file too large";
filelist_sz = 0; filelist_sz = 0;
} else { } else {
@@ -2089,9 +2089,10 @@ int httpmirror(char *url1, httrackp * opt) {
(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt), StringBuff(opt->path_log), (OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt), StringBuff(opt->path_log),
"hts-cache/old.lst"), "rb"); "hts-cache/old.lst"), "rb");
if (old_lst) { if (old_lst) {
const size_t sz = llint_to_size_t( const size_t sz =
fsize(fconcat(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt), off_t_to_size_t(fsize(fconcat
StringBuff(opt->path_log), "hts-cache/new.lst"))); (OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt), StringBuff(opt->path_log),
"hts-cache/new.lst")));
new_lst = new_lst =
fopen(fconcat fopen(fconcat
(OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt), StringBuff(opt->path_log), (OPT_GET_BUFF(opt), OPT_GET_BUFF_SIZE(opt), StringBuff(opt->path_log),

View File

@@ -1509,7 +1509,7 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
htsmain_free(); htsmain_free();
return -1; return -1;
} else { } else {
LLint fz; off_t fz;
na++; na++;
fz = fsize(argv[na]); fz = fsize(argv[na]);
@@ -2584,11 +2584,21 @@ static int hts_main_internal(int argc, char **argv, httrackp * opt) {
hts_get_version_info(opt), t, url); hts_get_version_info(opt), t, url);
fprintf(opt->log, "("); fprintf(opt->log, "(");
for(i = 0; i < argc; i++) { for(i = 0; i < argc; i++) {
const char *arg = argv[i]; // already UTF-8 on every platform #ifdef _WIN32
char *carg =
hts_convertStringSystemToUTF8(argv[i], (int) strlen(argv[i]));
char *arg = carg != NULL ? carg : argv[i];
#else
const char *arg = argv[i];
#endif
if (strchr(arg, ' ') == NULL || strchr(arg, '\"') != NULL) if (strchr(arg, ' ') == NULL || strchr(arg, '\"') != NULL)
fprintf(opt->log, "%s ", arg); fprintf(opt->log, "%s ", arg);
else // entre "" (si espace(s) et pas déja de ") else // entre "" (si espace(s) et pas déja de ")
fprintf(opt->log, "\"%s\" ", arg); fprintf(opt->log, "\"%s\" ", arg);
#ifdef _WIN32
if (carg != NULL)
free(carg);
#endif
} }
fprintf(opt->log, ")" LF); fprintf(opt->log, ")" LF);
fprintf(opt->log, LF); fprintf(opt->log, LF);

View File

@@ -122,11 +122,8 @@ int fa_strjoker_dual(int type, char **filters, int nfil, const char *nom1,
} }
/* Real filters/URLs fit HTS_URLMAXSIZE*2; past it a hostile pattern recurses /* Real filters/URLs fit HTS_URLMAXSIZE*2; past it a hostile pattern recurses
O(len) deep or runs O(n^2*stars). Cap length, recursion depth and steps O(len) deep or runs O(n^2*stars). Cap length (depth) and steps (work). */
(work): the length cap alone still allows ~2000 frames, ~900KB of stack,
which overflows the 1MB a Windows thread gets (#574). */
#define STRJOKER_MAXLEN (HTS_URLMAXSIZE * 2) #define STRJOKER_MAXLEN (HTS_URLMAXSIZE * 2)
#define STRJOKER_MAXDEPTH 256u
#define STRJOKER_MAXSTEPS 2000000u #define STRJOKER_MAXSTEPS 2000000u
/* Failure memo for the recursive matcher: one bit per (chaine, joker) offset /* Failure memo for the recursive matcher: one bit per (chaine, joker) offset
@@ -136,28 +133,20 @@ typedef struct strjoker_memo {
size_t stride; /* strlen(joker0) + 1 */ size_t stride; /* strlen(joker0) + 1 */
unsigned char *failed; /* failed-pair bitmap; NULL: no memo */ unsigned char *failed; /* failed-pair bitmap; NULL: no memo */
size_t *nsteps; /* shared work counter; NULL: unbounded (oracle) */ size_t *nsteps; /* shared work counter; NULL: unbounded (oracle) */
size_t maxdepth; /* deepest recursion reached */
hts_boolean cut; /* a branch hit the depth cap: its failures are not final */
} strjoker_memo; } strjoker_memo;
static const char *strjoker_impl(strjoker_memo *memo, const char *chaine, static const char *strjoker_impl(const strjoker_memo *memo, const char *chaine,
const char *joker, LLint *size, int *size_flag, const char *joker, LLint *size,
size_t depth); int *size_flag);
/* A pair that failed once fails forever (*size is only written on the success /* A pair that failed once fails forever (*size is only written on the success
path), so record NULL results and cut the re-exploration. */ path), so record NULL results and cut the re-exploration. */
static const char *strjoker_rec(strjoker_memo *memo, const char *chaine, static const char *strjoker_rec(const strjoker_memo *memo, const char *chaine,
const char *joker, LLint *size, int *size_flag, const char *joker, LLint *size,
size_t depth) { int *size_flag) {
size_t bit = 0; size_t bit = 0;
const char *adr; const char *adr;
if (depth > memo->maxdepth)
memo->maxdepth = depth;
if (depth >= STRJOKER_MAXDEPTH) {
memo->cut = HTS_TRUE;
return NULL; /* nesting beyond any real filter: fail the branch safely */
}
if (memo->nsteps != NULL && ++*memo->nsteps > STRJOKER_MAXSTEPS) if (memo->nsteps != NULL && ++*memo->nsteps > STRJOKER_MAXSTEPS)
return NULL; /* work budget spent: fail the match safely */ return NULL; /* work budget spent: fail the match safely */
if (memo->failed) { if (memo->failed) {
@@ -166,9 +155,8 @@ static const char *strjoker_rec(strjoker_memo *memo, const char *chaine,
if (memo->failed[bit >> 3] & (unsigned char) (1u << (bit & 7))) if (memo->failed[bit >> 3] & (unsigned char) (1u << (bit & 7)))
return NULL; return NULL;
} }
adr = strjoker_impl(memo, chaine, joker, size, size_flag, depth + 1); adr = strjoker_impl(memo, chaine, joker, size, size_flag);
/* a cut branch may fail here yet match when reached at a shallower depth */ if (adr == NULL && memo->failed)
if (adr == NULL && memo->failed && !memo->cut)
memo->failed[bit >> 3] |= (unsigned char) (1u << (bit & 7)); memo->failed[bit >> 3] |= (unsigned char) (1u << (bit & 7));
return adr; return adr;
} }
@@ -176,15 +164,13 @@ static const char *strjoker_rec(strjoker_memo *memo, const char *chaine,
/* Match chaine against joker with a shared work budget *nsteps (a strjokerfind /* Match chaine against joker with a shared work budget *nsteps (a strjokerfind
sweep passes one counter, bounding the whole scan). */ sweep passes one counter, bounding the whole scan). */
static const char *strjoker_bounded(const char *chaine, const char *joker, static const char *strjoker_bounded(const char *chaine, const char *joker,
LLint *size, int *size_flag, size_t *nsteps, LLint *size, int *size_flag,
size_t *maxdepth) { size_t *nsteps) {
strjoker_memo memo = {chaine, joker, 0, NULL, nsteps, 0, HTS_FALSE}; strjoker_memo memo = {chaine, joker, 0, NULL, nsteps};
unsigned char stackbits[2048]; unsigned char stackbits[2048];
hts_boolean onheap = HTS_FALSE; hts_boolean onheap = HTS_FALSE;
const char *adr; const char *adr;
if (maxdepth != NULL)
*maxdepth = 0;
if (chaine != NULL && joker != NULL) { if (chaine != NULL && joker != NULL) {
const size_t l1 = strlen(chaine), l2 = strlen(joker); const size_t l1 = strlen(chaine), l2 = strlen(joker);
@@ -203,11 +189,9 @@ static const char *strjoker_bounded(const char *chaine, const char *joker,
} }
} }
} }
adr = strjoker_rec(&memo, chaine, joker, size, size_flag, 0); adr = strjoker_rec(&memo, chaine, joker, size, size_flag);
if (onheap) if (onheap)
freet(memo.failed); freet(memo.failed);
if (maxdepth != NULL)
*maxdepth = memo.maxdepth;
return adr; return adr;
} }
@@ -218,39 +202,34 @@ HTS_INLINE const char *strjoker(const char *chaine, const char *joker,
LLint *size, int *size_flag) { LLint *size, int *size_flag) {
size_t nsteps = 0; size_t nsteps = 0;
return strjoker_bounded(chaine, joker, size, size_flag, &nsteps, NULL); return strjoker_bounded(chaine, joker, size, size_flag, &nsteps);
} }
/* Test-only oracle: the same matcher without the failure memo. */ /* Test-only oracle: the same matcher without the failure memo. */
const char *strjoker_nomemo(const char *chaine, const char *joker, LLint *size, const char *strjoker_nomemo(const char *chaine, const char *joker, LLint *size,
int *size_flag) { int *size_flag) {
strjoker_memo memo = {chaine, joker, 0, NULL, NULL, 0, HTS_FALSE}; const strjoker_memo memo = {chaine, joker, 0, NULL};
return strjoker_rec(&memo, chaine, joker, size, size_flag, 0); return strjoker_rec(&memo, chaine, joker, size, size_flag);
} }
/* Test-only: strjoker() reporting the work and depth spent and their caps, so a /* Test-only: strjoker() reporting the work-budget steps spent and the cap, so a
self-test can prove both bound a hostile pattern. */ self-test can prove the budget bounds a hostile pattern's work. */
const char *strjoker_bounds(const char *chaine, const char *joker, const char *strjoker_steps(const char *chaine, const char *joker,
size_t *nsteps_out, size_t *maxsteps_out, size_t *nsteps_out, size_t *maxsteps_out) {
size_t *depth_out, size_t *maxdepth_out) { size_t nsteps = 0;
size_t nsteps = 0, depth = 0; const char *r = strjoker_bounded(chaine, joker, NULL, NULL, &nsteps);
const char *r = strjoker_bounded(chaine, joker, NULL, NULL, &nsteps, &depth);
if (nsteps_out != NULL) if (nsteps_out != NULL)
*nsteps_out = nsteps; *nsteps_out = nsteps;
if (maxsteps_out != NULL) if (maxsteps_out != NULL)
*maxsteps_out = STRJOKER_MAXSTEPS; *maxsteps_out = STRJOKER_MAXSTEPS;
if (depth_out != NULL)
*depth_out = depth;
if (maxdepth_out != NULL)
*maxdepth_out = STRJOKER_MAXDEPTH;
return r; return r;
} }
static const char *strjoker_impl(strjoker_memo *memo, const char *chaine, static const char *strjoker_impl(const strjoker_memo *memo, const char *chaine,
const char *joker, LLint *size, int *size_flag, const char *joker, LLint *size,
size_t depth) { 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;
@@ -424,8 +403,7 @@ static const char *strjoker_impl(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_rec(memo, chaine, joker + jmp, size, size_flag))) {
depth))) {
return adr; return adr;
} }
} }
@@ -438,7 +416,7 @@ static const char *strjoker_impl(strjoker_memo *memo, const char *chaine,
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_rec(memo, chaine + i + 1, joker + jmp, size,
size_flag, depth))) { size_flag))) {
return adr; return adr;
} }
i++; i++;
@@ -449,7 +427,7 @@ static const char *strjoker_impl(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_rec(memo, chaine + max, joker + jmp, size,
size_flag, depth))) size_flag)))
return adr; return adr;
return NULL; // perdu return NULL; // perdu
@@ -471,8 +449,7 @@ static const char *strjoker_impl(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_rec(memo, chaine + jmp, joker + jmp, size, size_flag))
depth))
return chaine; // retourner 1e lettre return chaine; // retourner 1e lettre
} }
@@ -494,8 +471,8 @@ const char *strjokerfind(const char *chaine, const char *joker) {
if (chaine != NULL && strlen(chaine) > STRJOKER_MAXLEN) if (chaine != NULL && strlen(chaine) > STRJOKER_MAXLEN)
return NULL; return NULL;
while(*chaine) { while(*chaine) {
if ((adr = strjoker_bounded(chaine, joker, NULL, NULL, &nsteps, if ((adr = strjoker_bounded(chaine, joker, NULL, NULL,
NULL))) { // ok trouvé &nsteps))) { // ok trouvé
return adr; return adr;
} }
if (nsteps > STRJOKER_MAXSTEPS) // scan budget spent: no match if (nsteps > STRJOKER_MAXSTEPS) // scan budget spent: no match

View File

@@ -52,12 +52,10 @@ HTS_INLINE const char *strjoker(const char *chaine, const char *joker, LLint * s
oracle for the memoized matcher. */ oracle for the memoized matcher. */
const char *strjoker_nomemo(const char *chaine, const char *joker, LLint *size, const char *strjoker_nomemo(const char *chaine, const char *joker, LLint *size,
int *size_flag); int *size_flag);
/* strjoker() reporting the work-budget steps and the recursion depth it spent, /* strjoker() reporting the work-budget steps it spent and the cap; test-only,
with their caps; test-only, lets a self-test assert both bound a hostile lets a self-test assert the budget bounds a hostile pattern's work. */
pattern (depth bounds the stack: an uncapped one overflows Windows' 1MB). */ const char *strjoker_steps(const char *chaine, const char *joker,
const char *strjoker_bounds(const char *chaine, const char *joker, size_t *nsteps_out, size_t *maxsteps_out);
size_t *nsteps_out, size_t *maxsteps_out,
size_t *depth_out, size_t *maxdepth_out);
const char *strjokerfind(const char *chaine, const char *joker); const char *strjokerfind(const char *chaine, const char *joker);
#endif #endif

View File

@@ -322,15 +322,16 @@ typedef int64_t TStamp;
/* Full printf conversion, '%' included (PRId64 has none): "X: " LLintP. */ /* Full printf conversion, '%' included (PRId64 has none): "X: " LLintP. */
#define LLintP "%" PRId64 #define LLintP "%" PRId64
/* Integer type for file offsets/sizes passed to the C library; INTsysP is its /* Integer type for file offsets/sizes passed to the C library. Widens to
printf conversion. FIXME: LFS_FLAG is a configure make variable, never a C LLint (with HTS_FSEEKO for fseeko/ftello) under large-file support, plain
macro, so this test is dead and INTsys stays int on POSIX despite large-file int otherwise; INTsysP is its printf conversion. */
support (the real macro is HTS_LFS). Widening it there is an installed-header #ifdef LFS_FLAG
type change, so it is left alone here. */
#if defined(LFS_FLAG) || defined(_MSC_VER)
typedef LLint INTsys; typedef LLint INTsys;
#define INTsysP LLintP #define INTsysP LLintP
#ifdef __linux
#define HTS_FSEEKO
#endif
#else #else
typedef int INTsys; typedef int INTsys;

View File

@@ -318,7 +318,7 @@ void index_finish(const char *indexpath, int mode) {
char catbuff[CATBUFF_SIZE]; char catbuff[CATBUFF_SIZE];
char **tab; char **tab;
char *blk; char *blk;
LLint size = fpsize(fp_tmpproject); off_t size = fpsize(fp_tmpproject);
if (size > 0) { if (size > 0) {
if (fp_tmpproject) { if (fp_tmpproject) {

View File

@@ -4591,31 +4591,26 @@ int fexist_utf8(const char *s) {
return 0; return 0;
} }
/* File size, -1 if absent */ /* Taille d'un fichier, -1 si n'existe pas */
/* Note: NOT utf-8 */ /* Note: NOT utf-8 */
LLint fsize(const char *s) { off_t fsize(const char *s) {
STRUCT_STAT st; struct stat st;
if (!strnotempty(s)) // empty name: error if (!strnotempty(s)) // nom vide: erreur
return -1; return -1;
/* _stat64, not stat(): the latter is 32-bit on MSVC */
#ifdef _WIN32
if (_stat64(s, &st) == 0 && S_ISREG(st.st_mode)) {
#else
if (stat(s, &st) == 0 && S_ISREG(st.st_mode)) { if (stat(s, &st) == 0 && S_ISREG(st.st_mode)) {
#endif
return st.st_size; return st.st_size;
} else { } else {
return -1; return -1;
} }
} }
/* File size, -1 if absent */ /* Taille d'un fichier, -1 si n'existe pas */
/* Note: utf-8 */ /* Note: utf-8 */
LLint fsize_utf8(const char *s) { off_t fsize_utf8(const char *s) {
STRUCT_STAT st; STRUCT_STAT st;
if (!strnotempty(s)) // empty name: error if (!strnotempty(s)) // nom vide: erreur
return -1; return -1;
if (STAT(s, &st) == 0 && S_ISREG(st.st_mode)) { if (STAT(s, &st) == 0 && S_ISREG(st.st_mode)) {
return st.st_size; return st.st_size;
@@ -4624,17 +4619,24 @@ LLint fsize_utf8(const char *s) {
} }
} }
/* fseeko/ftello, never fseek/ftell: ftell returns long, which cannot carry an off_t fpsize(FILE * fp) {
offset past 2GB on a 32-bit platform and fails with EOVERFLOW there. */ off_t oldpos, size;
LLint fpsize(FILE *fp) {
LLint oldpos, size;
if (!fp) if (!fp)
return -1; return -1;
#ifdef HTS_FSEEKO
oldpos = ftello(fp); oldpos = ftello(fp);
fseeko(fp, 0, SEEK_END); #else
oldpos = ftell(fp);
#endif
fseek(fp, 0, SEEK_END);
#ifdef HTS_FSEEKO
size = ftello(fp); size = ftello(fp);
fseeko(fp, oldpos, SEEK_SET); fseeko(fp, oldpos, SEEK_SET);
#else
size = ftell(fp);
fseek(fp, oldpos, SEEK_SET);
#endif
return size; return size;
} }
@@ -6398,13 +6400,13 @@ int hts_stat_utf8(const char *path, STRUCT_STAT * buf) {
LPWSTR wpath = hts_convertUTF8StringToUCS2(path, (int) strlen(path), NULL); LPWSTR wpath = hts_convertUTF8StringToUCS2(path, (int) strlen(path), NULL);
if (wpath != NULL) { if (wpath != NULL) {
const int result = _wstat64(wpath, buf); const int result = _wstat(wpath, buf);
free(wpath); free(wpath);
return result; return result;
} else { } else {
// Fallback on conversion error. // Fallback on conversion error.
return _stat64(path, buf); return _stat(path, buf);
} }
} }

View File

@@ -310,11 +310,10 @@ void cut_path(char *fullpath, char *path, size_t path_size, char *pname,
int fexist(const char *s); int fexist(const char *s);
int fexist_utf8(const char *s); int fexist_utf8(const char *s);
/* File size in bytes, -1 if absent or not a regular file. LLint, not off_t: /*LLint fsize(const char* s); */
the latter is a 32-bit long on MSVC, truncating any file of 2GB or more. */ off_t fpsize(FILE * fp);
LLint fpsize(FILE *fp); off_t fsize(const char *s);
LLint fsize(const char *s); off_t fsize_utf8(const char *s);
LLint fsize_utf8(const char *s);
// Threads // Threads
typedef void *(*beginthread_type) (void *); typedef void *(*beginthread_type) (void *);
@@ -394,8 +393,7 @@ void *hts_get_callback(t_hts_htmlcheck_callbacks * callbacks,
HTSEXT_API FILE *hts_fopen_utf8(const char *path, const char *mode); HTSEXT_API FILE *hts_fopen_utf8(const char *path, const char *mode);
#define STAT hts_stat_utf8 #define STAT hts_stat_utf8
/* _stat64: _stat's st_size is a 32-bit long, even in x64 builds */ typedef struct _stat STRUCT_STAT;
typedef struct _stat64 STRUCT_STAT;
HTSEXT_API int hts_stat_utf8(const char *path, STRUCT_STAT * buf); HTSEXT_API int hts_stat_utf8(const char *path, STRUCT_STAT * buf);
#define UNLINK hts_unlink_utf8 #define UNLINK hts_unlink_utf8
@@ -593,9 +591,9 @@ HTS_STATIC int compare_mime(httrackp * opt, const char *mime, const char *file,
#endif #endif
// returns (size_t) -1 upon error // returns (size_t) -1 upon error
static HTS_UNUSED size_t llint_to_size_t(LLint o) { static HTS_UNUSED size_t off_t_to_size_t(off_t o) {
const size_t so = (size_t) o; const size_t so = (size_t) o;
if ((LLint) so == o) { if ((off_t) so == o) {
return so; return so;
} else { } else {
return (size_t) -1; return (size_t) -1;

View File

@@ -65,7 +65,6 @@ Please visit our Website: http://www.httrack.com
#include "coucal/coucal.h" #include "coucal/coucal.h"
#include <ctype.h> #include <ctype.h>
#include <errno.h>
#include <stdint.h> #include <stdint.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
@@ -73,9 +72,6 @@ Please visit our Website: http://www.httrack.com
#ifndef _WIN32 #ifndef _WIN32
#include <sys/socket.h> #include <sys/socket.h>
#include <unistd.h> #include <unistd.h>
#else
#include <io.h> /* _get_osfhandle, for the sparse-file hint */
#include <winioctl.h> /* FSCTL_SET_SPARSE */
#endif #endif
/* very minimalistic internal tests */ /* very minimalistic internal tests */
@@ -757,7 +753,7 @@ static int st_filterbounds(httrackp *opt, int argc, char **argv) {
const size_t subjlen = 2048; const size_t subjlen = 2048;
char *subj = malloct(big + 1); char *subj = malloct(big + 1);
char *pat = malloct(2 * stars + 2); char *pat = malloct(2 * stars + 2);
size_t steps = 0, maxsteps = 0, depth = 0, maxdepth = 0, i; size_t steps = 0, maxsteps = 0, i;
(void) opt; (void) opt;
(void) argc; (void) argc;
@@ -778,22 +774,9 @@ static int st_filterbounds(httrackp *opt, int argc, char **argv) {
subj[subjlen] = '\0'; subj[subjlen] = '\0';
/* Budget must fire and hold: steps > cap (deleting the budget zeroes the /* Budget must fire and hold: steps > cap (deleting the budget zeroes the
counter that is the enforcement), steps < 10*cap (unbudgeted ~1.26e9). */ counter that is the enforcement), steps < 10*cap (unbudgeted ~1.26e9). */
assertf(strjoker_bounds(subj, pat, &steps, &maxsteps, &depth, &maxdepth) == assertf(strjoker_steps(subj, pat, &steps, &maxsteps) == NULL);
NULL);
assertf(steps > maxsteps && steps < 10 * maxsteps); assertf(steps > maxsteps && steps < 10 * maxsteps);
/* Depth caps the stack: uncapped this recurses 2046 frames, ~900KB (#574). */
assertf(depth == maxdepth);
assertf(strjokerfind(subj, pat) == NULL); assertf(strjokerfind(subj, pat) == NULL);
/* Pin the cap from below: 32 segments must still match, so a cap set so low
it would break real multi-segment filters (which use far fewer) fails. */
for (i = 0; i < 32; i++) {
pat[2 * i] = '*';
pat[2 * i + 1] = 'a';
}
pat[64] = '\0';
memset(subj, 'a', 32);
subj[32] = '\0';
assertf(strjoker(subj, pat, NULL, NULL) != NULL);
freet(pat); freet(pat);
freet(subj); freet(subj);
printf("filterbounds: OK\n"); printf("filterbounds: OK\n");
@@ -830,11 +813,7 @@ static int st_mime(httrackp *opt, int argc, char **argv) {
return 0; return 0;
} }
static size_t st_decode_body(const char *arg, char *buf, size_t size);
static int st_charset(httrackp *opt, int argc, char **argv) { static int st_charset(httrackp *opt, int argc, char **argv) {
char buf[512];
size_t len;
char *s; char *s;
(void) opt; (void) opt;
@@ -842,8 +821,7 @@ static int st_charset(httrackp *opt, int argc, char **argv) {
fprintf(stderr, "charset: needs a charset and a string\n"); fprintf(stderr, "charset: needs a charset and a string\n");
return 1; return 1;
} }
len = st_decode_body(argv[1], buf, sizeof(buf)); s = hts_convertStringToUTF8(argv[1], strlen(argv[1]), argv[0]);
s = hts_convertStringToUTF8(buf, len, argv[0]);
if (s != NULL) { if (s != NULL) {
printf("%s\n", s); printf("%s\n", s);
freet(s); freet(s);
@@ -868,22 +846,16 @@ static int st_metacharset(httrackp *opt, int argc, char **argv) {
} }
static int st_isutf8(httrackp *opt, int argc, char **argv) { static int st_isutf8(httrackp *opt, int argc, char **argv) {
char buf[512];
size_t len;
(void) opt; (void) opt;
if (argc < 1) { if (argc < 1) {
fprintf(stderr, "isutf8: needs a string\n"); fprintf(stderr, "isutf8: needs a string\n");
return 1; return 1;
} }
len = st_decode_body(argv[0], buf, sizeof(buf)); printf("%d\n", hts_isStringUTF8(argv[0], strlen(argv[0])) ? 1 : 0);
printf("%d\n", hts_isStringUTF8(buf, len) ? 1 : 0);
return 0; return 0;
} }
static int st_idna_encode(httrackp *opt, int argc, char **argv) { static int st_idna_encode(httrackp *opt, int argc, char **argv) {
char buf[512];
size_t len;
char *s; char *s;
(void) opt; (void) opt;
@@ -891,8 +863,7 @@ static int st_idna_encode(httrackp *opt, int argc, char **argv) {
fprintf(stderr, "idna-encode: needs a hostname\n"); fprintf(stderr, "idna-encode: needs a hostname\n");
return 1; return 1;
} }
len = st_decode_body(argv[0], buf, sizeof(buf)); s = hts_convertStringUTF8ToIDNA(argv[0], strlen(argv[0]));
s = hts_convertStringUTF8ToIDNA(buf, len);
if (s != NULL) { if (s != NULL) {
printf("%s\n", s); printf("%s\n", s);
freet(s); freet(s);
@@ -1054,7 +1025,7 @@ static int st_hashtable(httrackp *opt, int argc, char **argv) {
/* produce random patterns, or read from a file */ /* produce random patterns, or read from a file */
if (sscanf(snum, "%lu", &count) != 1) { if (sscanf(snum, "%lu", &count) != 1) {
const LLint size = fsize(snum); const off_t size = fsize(snum);
FILE *fp = fopen(snum, "rb"); FILE *fp = fopen(snum, "rb");
if (fp != NULL) { if (fp != NULL) {
buff = malloct(size); buff = malloct(size);
@@ -1154,10 +1125,7 @@ static int st_strsafe(httrackp *opt, int argc, char **argv) {
comes from argv so its length is opaque to the compiler (no static comes from argv so its length is opaque to the compiler (no static
-Wstringop-overflow, genuine runtime check). "overflow-buff" exercises -Wstringop-overflow, genuine runtime check). "overflow-buff" exercises
htsbuff. */ htsbuff. */
/* Not sizeof(char*): on ILP32 a char[4] equals the pointer size, and the char small[4];
MSVC array-vs-pointer heuristic (sizeof(A) != sizeof(char*)) then reads
it as a pointer and silently skips the bound. */
char small[6];
const char *const src = (argc >= 2) ? argv[1] : "overflowing"; const char *const src = (argc >= 2) ? argv[1] : "overflowing";
if (strcmp(argv[0], "overflow-buff") == 0) { if (strcmp(argv[0], "overflow-buff") == 0) {
@@ -1641,8 +1609,7 @@ static int st_crange(httrackp *opt, int argc, char **argv) {
return 0; return 0;
} }
/* Decode an argument ("hex:FFD8.." or literal text) into buf. Raw non-UTF-8 /* Decode a body argument ("hex:FFD8.." or literal text) into buf. */
bytes cannot survive a Windows command line, so hostile inputs go as hex. */
static size_t st_decode_body(const char *arg, char *buf, size_t size) { static size_t st_decode_body(const char *arg, char *buf, size_t size) {
size_t n = 0; size_t n = 0;
@@ -1682,94 +1649,6 @@ static int st_sniff(httrackp *opt, int argc, char **argv) {
return 0; return 0;
} }
/* fsize()/fsize_utf8()/fpsize() must report a size past 4GB: 32-bit wraps both
ways there (MSVC's off_t and struct _stat st_size are long, 32-bit even on
x64), and a size under 4GB would survive an *unsigned* 32-bit truncation. */
static int st_fsize(httrackp *opt, int argc, char **argv) {
const LLint expected = 5 * 1024 * 1024 * 1024LL;
char BIGSTK path[HTS_URLMAXSIZE * 2];
char BIGSTK absent[HTS_URLMAXSIZE * 2];
/* both variants: only fsize() feeds the >2GB readers (file://, --list) */
const int width = (int) sizeof(fsize(""));
const int width_utf8 = (int) sizeof(fsize_utf8(""));
FILE *fp;
LLint got, got_utf8, gotp, gone;
int rc = 0;
(void) opt;
if (argc < 1) {
fprintf(stderr, "fsize: needs a directory\n");
return 1;
}
concat(path, sizeof(path), argv[0], "/sparse-5g.bin");
concat(absent, sizeof(absent), argv[0], "/no-such-file.bin");
/* sparse: seek past 4GB and write the last byte */
fp = FOPEN(path, "wb");
if (fp == NULL) {
fprintf(stderr, "fsize: cannot create '%s': %s\n", path, strerror(errno));
return 1;
}
#ifdef _WIN32
{
/* NTFS allocates the hole unless asked not to; POSIX gives it for free.
Best-effort: a non-sparse file still measures the same, just costs 5GB.
*/
HANDLE h = (HANDLE) _get_osfhandle(_fileno(fp));
DWORD ret;
if (h != INVALID_HANDLE_VALUE)
(void) DeviceIoControl(h, FSCTL_SET_SPARSE, NULL, 0, NULL, 0, &ret, NULL);
}
#endif
if (fseeko(fp, expected - 1, SEEK_SET) != 0 || fputc(0, fp) == EOF ||
fclose(fp) != 0) {
fprintf(stderr, "fsize: cannot extend '%s' to " LLintP ": %s\n", path,
expected, strerror(errno));
UNLINK(path);
return 1;
}
got = fsize(path);
got_utf8 = fsize_utf8(path);
fp = FOPEN(path, "rb");
if (fp == NULL) {
fprintf(stderr, "fsize: cannot reopen '%s': %s\n", path, strerror(errno));
gotp = -1;
} else {
gotp = fpsize(fp);
fclose(fp);
}
UNLINK(path);
gone = fsize(absent); /* contract: -1, not 0, when absent */
printf("fsize: width=%d,%d size=" LLintP "," LLintP " psize=" LLintP
" absent=" LLintP "\n",
width, width_utf8, got, got_utf8, gotp, gone);
if (width != 8 || width_utf8 != 8) {
fprintf(stderr, "fsize: return types are %d/%d bytes, expected 8\n", width,
width_utf8);
rc = 1;
}
if (got != expected || got_utf8 != expected) {
fprintf(stderr,
"fsize: fsize/fsize_utf8 are " LLintP "/" LLintP
", expected " LLintP "\n",
got, got_utf8, expected);
rc = 1;
}
if (gotp != expected) {
fprintf(stderr, "fsize: fpsize is " LLintP ", expected " LLintP "\n", gotp,
expected);
rc = 1;
}
if (gone != -1) {
fprintf(stderr, "fsize: absent file is " LLintP ", expected -1\n", gone);
rc = 1;
}
return rc;
}
static int st_savename(httrackp *opt, int argc, char **argv) { static int st_savename(httrackp *opt, int argc, char **argv) {
lien_adrfilsave afs; lien_adrfilsave afs;
cache_back cache; cache_back cache;
@@ -2916,43 +2795,11 @@ static int st_robots(httrackp *opt, int argc, char **argv) {
return 0; return 0;
} }
/* Connected stream pair over loopback; Windows has no socketpair(). */
static int st_socketpair(T_SOC sv[2]) {
struct sockaddr_in sa;
socklen_t len = sizeof(sa);
T_SOC srv, cli, acc;
memset(&sa, 0, sizeof(sa));
sa.sin_family = AF_INET;
sa.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
if ((srv = (T_SOC) socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET)
return -1;
if (bind(srv, (struct sockaddr *) &sa, sizeof(sa)) != 0 ||
listen(srv, 1) != 0 ||
getsockname(srv, (struct sockaddr *) &sa, &len) != 0) {
deletesoc(srv);
return -1;
}
if ((cli = (T_SOC) socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) {
deletesoc(srv);
return -1;
}
if (connect(cli, (struct sockaddr *) &sa, sizeof(sa)) != 0 ||
(acc = (T_SOC) accept(srv, NULL, NULL)) == INVALID_SOCKET) {
deletesoc(cli);
deletesoc(srv);
return -1;
}
deletesoc(srv);
sv[0] = acc;
sv[1] = cli;
return 0;
}
/* get_ftp_line must bound a hostile, CRLF-less reply into its internal /* get_ftp_line must bound a hostile, CRLF-less reply into its internal
1024-byte buffer; ASan turns the pre-fix overflow into an abort here. */ 1024-byte buffer; ASan turns the pre-fix overflow into an abort here. */
#ifndef _WIN32
static int st_ftpline(httrackp *opt, int argc, char **argv) { static int st_ftpline(httrackp *opt, int argc, char **argv) {
T_SOC sv[2]; int sv[2];
char line[2048]; char line[2048];
char flood[4096]; char flood[4096];
@@ -2960,19 +2807,19 @@ static int st_ftpline(httrackp *opt, int argc, char **argv) {
(void) argc; (void) argc;
(void) argv; (void) argv;
memset(flood, 'x', sizeof(flood)); memset(flood, 'x', sizeof(flood));
assertf(st_socketpair(sv) == 0); assertf(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0);
// the 4102-byte reply fits the loopback send buffer, so no reader is needed assertf(write(sv[1], "220 ", 4) == 4); // valid 3-digit code
assertf(send(sv[1], "220 ", 4, 0) == 4); // valid 3-digit code assertf(write(sv[1], flood, sizeof(flood)) == (ssize_t) sizeof(flood));
assertf(send(sv[1], flood, (int) sizeof(flood), 0) == (int) sizeof(flood)); assertf(write(sv[1], "\r\n", 2) == 2); // end the line so we return
assertf(send(sv[1], "\r\n", 2, 0) == 2); // end the line so we return close(sv[1]);
deletesoc(sv[1]);
line[0] = '\0'; line[0] = '\0';
get_ftp_line(sv[0], line, sizeof(line), 5); get_ftp_line(sv[0], line, sizeof(line), 5);
deletesoc(sv[0]); close(sv[0]);
printf("ftp-line self-test OK (bounded %d-byte reply)\n", printf("ftp-line self-test OK (bounded %d-byte reply)\n",
(int) sizeof(flood)); (int) sizeof(flood));
return 0; return 0;
} }
#endif
/* ftp_split_userpass: well-formed split, plus a hostile over-long userinfo /* ftp_split_userpass: well-formed split, plus a hostile over-long userinfo
that pre-fix overran user[256]/pass[256]. */ that pre-fix overran user[256]/pass[256]. */
@@ -3044,11 +2891,11 @@ static const struct selftest_entry {
{"redirect-samefile", "", "same-file redirect detection self-test (#159)", {"redirect-samefile", "", "same-file redirect detection self-test (#159)",
st_redirect_samefile}, st_redirect_samefile},
{"mime", "<filename>", "MIME type for a filename", st_mime}, {"mime", "<filename>", "MIME type for a filename", st_mime},
{"charset", "<charset> <hex:..|string>", {"charset", "<charset> <string>",
"convert a string to UTF-8 from a charset", st_charset}, "convert a string to UTF-8 from a charset", st_charset},
{"metacharset", "<html>", "extract the <meta> charset from an HTML page", {"metacharset", "<html>", "extract the <meta> charset from an HTML page",
st_metacharset}, st_metacharset},
{"isutf8", "<hex:..|string>", "is the string valid UTF-8 (1/0)", st_isutf8}, {"isutf8", "<string>", "is the string valid UTF-8 (1/0)", st_isutf8},
{"idna-encode", "<host>", "encode a hostname to IDNA/punycode", {"idna-encode", "<host>", "encode a hostname to IDNA/punycode",
st_idna_encode}, st_idna_encode},
{"idna-decode", "<host>", "decode an IDNA/punycode hostname", {"idna-decode", "<host>", "decode an IDNA/punycode hostname",
@@ -3085,7 +2932,6 @@ static const struct selftest_entry {
"local save-name for a URL", st_savename}, "local save-name for a URL", st_savename},
{"sniff", "<content-type> <hex:..|text>", "MIME magic consistency", {"sniff", "<content-type> <hex:..|text>", "MIME magic consistency",
st_sniff}, st_sniff},
{"fsize", "<dir>", "file size past the 2GB signed-32-bit wrap", st_fsize},
{"cache", "<dir>", "cache read/write round-trip self-test", st_cache}, {"cache", "<dir>", "cache read/write round-trip self-test", st_cache},
{"cacheindex", "", "cache-index (.ndx) parse must stay in bounds", {"cacheindex", "", "cache-index (.ndx) parse must stay in bounds",
st_cacheindex}, st_cacheindex},
@@ -3116,8 +2962,10 @@ static const struct selftest_entry {
st_contentcodings}, st_contentcodings},
{"robots", "", "robots.txt RFC 9309 Allow/Disallow precedence self-test", {"robots", "", "robots.txt RFC 9309 Allow/Disallow precedence self-test",
st_robots}, st_robots},
#ifndef _WIN32
{"ftp-line", "", "get_ftp_line bounds a hostile FTP reply line", {"ftp-line", "", "get_ftp_line bounds a hostile FTP reply line",
st_ftpline}, st_ftpline},
#endif
{"ftp-userpass", "", "ftp_split_userpass bounds URL userinfo", st_ftpuser}, {"ftp-userpass", "", "ftp_split_userpass bounds URL userinfo", st_ftpuser},
}; };

View File

@@ -743,8 +743,7 @@ HTSEXT_API hts_boolean hts_findissystem(find_handle find);
HTSEXT_API FILE *hts_fopen_utf8(const char *path, const char *mode); HTSEXT_API FILE *hts_fopen_utf8(const char *path, const char *mode);
#define STAT hts_stat_utf8 #define STAT hts_stat_utf8
/* _stat64: _stat's st_size is a 32-bit long, even in x64 builds */ typedef struct _stat STRUCT_STAT;
typedef struct _stat64 STRUCT_STAT;
HTSEXT_API int hts_stat_utf8(const char *path, STRUCT_STAT *buf); HTSEXT_API int hts_stat_utf8(const char *path, STRUCT_STAT *buf);

View File

@@ -45,7 +45,6 @@ Please visit our Website: http://www.httrack.com
#include "htsdefines.h" #include "htsdefines.h"
#include "httrack.h" #include "httrack.h"
#include "htslib.h" #include "htslib.h"
#include "htscharset.h" // after htslib.h: winsock2.h must precede windows.h
/* Static definitions */ /* Static definitions */
static int fexist(const char *s); static int fexist(const char *s);
@@ -207,7 +206,6 @@ int main(int argc, char **argv) {
httrackp *opt; httrackp *opt;
#ifdef _WIN32 #ifdef _WIN32
hts_argv_utf8(&argc, &argv);
{ {
WORD wVersionRequested; // requested version WinSock API WORD wVersionRequested; // requested version WinSock API
WSADATA wsadata; // Windows Sockets API data WSADATA wsadata; // Windows Sockets API data

View File

@@ -70,7 +70,7 @@
<Link> <Link>
<!-- vcpkg auto-links libssl/libcrypto/zlib/brotli/zstd; only the OS import <!-- vcpkg auto-links libssl/libcrypto/zlib/brotli/zstd; only the OS import
lib is explicit. --> lib is explicit. -->
<AdditionalDependencies>Ws2_32.lib;Shell32.lib;%(AdditionalDependencies)</AdditionalDependencies> <AdditionalDependencies>Ws2_32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<GenerateDebugInformation>true</GenerateDebugInformation> <GenerateDebugInformation>true</GenerateDebugInformation>
</Link> </Link>
</ItemDefinitionGroup> </ItemDefinitionGroup>

View File

@@ -577,11 +577,9 @@ PT_Index PT_LoadCache(const char *filename) {
chain = coucal_enum_next(&en); chain = coucal_enum_next(&en);
} }
if (chain != NULL) { if (chain != NULL) {
const char *scheme = link_has_authority(chain->name) ? "" : "http://"; if (!link_has_authority(chain->name))
strcat(index->slots.common.startUrl, "http://");
snprintf(index->slots.common.startUrl, strcat(index->slots.common.startUrl, chain->name);
sizeof(index->slots.common.startUrl), "%s%s", scheme,
(const char *) chain->name);
} }
} }
} }
@@ -634,7 +632,7 @@ int PT_EnumCache(PT_Indexes indexes,
const long int index_id = (long int) chain->value.intg; const long int index_id = (long int) chain->value.intg;
const char *const url = chain->name; const char *const url = chain->name;
if (index_id >= 0 && index_id < indexes->index_size) { if (index_id >= 0 && index_id <= indexes->index_size) {
PT_Element item = PT_Element item =
PT_ReadCache(indexes->index[index_id], url, PT_ReadCache(indexes->index[index_id], url,
FETCH_HEADERS | FETCH_BODY); FETCH_HEADERS | FETCH_BODY);
@@ -759,7 +757,7 @@ PT_Element PT_ReadIndex(PT_Indexes indexes, const char *url, int flags) {
if (strncmp(url, "http://", 7) == 0) if (strncmp(url, "http://", 7) == 0)
url += 7; url += 7;
if (coucal_read(indexes->cil, url, &index_id)) { if (coucal_read(indexes->cil, url, &index_id)) {
if (index_id >= 0 && index_id < indexes->index_size) { if (index_id >= 0 && index_id <= indexes->index_size) {
PT_Element item = PT_ReadCache(indexes->index[index_id], url, flags); PT_Element item = PT_ReadCache(indexes->index[index_id], url, flags);
if (item != NULL) { if (item != NULL) {
@@ -782,7 +780,7 @@ int PT_LookupIndex(PT_Indexes indexes, const char *url) {
if (strncmp(url, "http://", 7) == 0) if (strncmp(url, "http://", 7) == 0)
url += 7; url += 7;
if (coucal_read(indexes->cil, url, &index_id)) { if (coucal_read(indexes->cil, url, &index_id)) {
if (index_id >= 0 && index_id < indexes->index_size) { if (index_id >= 0 && index_id <= indexes->index_size) {
return 1; return 1;
} else { } else {
proxytrack_print_log(CRITICAL, proxytrack_print_log(CRITICAL,
@@ -969,12 +967,10 @@ int PT_LoadCache__New(PT_Index index_, const char *filename) {
/* First link as starting URL */ /* First link as starting URL */
if (!firstSeen) { if (!firstSeen) {
if (strstr(filenameIndex, "/robots.txt") == NULL) { if (strstr(filenameIndex, "/robots.txt") == NULL) {
const char *scheme =
link_has_authority(filenameIndex) ? "" : "http://";
firstSeen = 1; firstSeen = 1;
snprintf(index->startUrl, sizeof(index->startUrl), "%s%s", if (!link_has_authority(filenameIndex))
scheme, filenameIndex); strcat(index->startUrl, "http://");
strcat(index->startUrl, filenameIndex);
} }
} }
} else { } else {
@@ -1106,8 +1102,7 @@ static PT_Element PT_ReadCache__New_u(PT_Index index_, const char *url,
&& (!isalpha(previous_save_[0]) || previous_save_[1] != ':')) // c:/home/foo/bar.gif && (!isalpha(previous_save_[0]) || previous_save_[1] != ':')) // c:/home/foo/bar.gif
) { ) {
index->safeCache = 1; index->safeCache = 1;
snprintf(previous_save, sizeof(previous_save), "%s%s", sprintf(previous_save, "%s%s", index->path, previous_save_);
index->path, previous_save_);
} }
// bogus format (includes buggy absolute path) // bogus format (includes buggy absolute path)
else { else {
@@ -1132,16 +1127,15 @@ static PT_Element PT_ReadCache__New_u(PT_Index index_, const char *url,
int saveLen = (int) strlen(previous_save_); int saveLen = (int) strlen(previous_save_);
if (index->fixedPath < saveLen) { if (index->fixedPath < saveLen) {
snprintf(previous_save, sizeof(previous_save), "%s%s", sprintf(previous_save, "%s%s", index->path,
index->path, previous_save_ + index->fixedPath); previous_save_ + index->fixedPath);
} else { } else {
snprintf(r->msg, sizeof(r->msg), "Bogus fixePath prefix for %s (prefixLen=%d)", snprintf(r->msg, sizeof(r->msg), "Bogus fixePath prefix for %s (prefixLen=%d)",
previous_save_, (int) index->fixedPath); previous_save_, (int) index->fixedPath);
r->statuscode = STATUSCODE_INVALID; r->statuscode = STATUSCODE_INVALID;
} }
} else { } else {
snprintf(previous_save, sizeof(previous_save), "%s%s", sprintf(previous_save, "%s%s", index->path, previous_save_);
index->path, previous_save_);
} }
} }
} }
@@ -1354,71 +1348,45 @@ static int PT_SaveCache__New(PT_Indexes indexes, const char *filename) {
/* Old HTTrack cache (dat/ndx) format */ /* Old HTTrack cache (dat/ndx) format */
/* ------------------------------------------------------------ */ /* ------------------------------------------------------------ */
static int cache_brstr(char *adr, char *s, size_t s_size) { static int cache_brstr(char *adr, char *s) {
int i; int i;
int off; int off;
char buff[256 + 4]; char buff[256 + 1];
off = binput(adr, buff, 256); off = binput(adr, buff, 256);
/* binput stops at adr's terminating NUL; a value only follows a real line adr += off;
terminator, so never step past end-of-buffer. */ sscanf(buff, "%d", &i);
if (adr[off - 1] == '\0') { if (i > 0)
s[0] = '\0'; strncpy(s, adr, i);
return off - 1; *(s + i) = '\0';
} off += i;
/* an empty/non-numeric field leaves i unset: treat as length 0 */
if (sscanf(buff, "%d", &i) != 1 || i < 0 || i > 32768)
i = 0;
if (i > 0) {
/* a corrupt cache may declare a length past the buffer end; bound the copy
and the advance to the bytes actually present */
const size_t avail = strnlen(adr + off, (size_t) i);
const size_t store = avail < s_size ? avail : s_size - 1;
memcpy(s, adr + off, store);
s[store] = '\0';
off += (int) avail;
} else {
s[0] = '\0';
}
return off; return off;
} }
static void cache_rstr(FILE *fp, char *s, size_t s_size) { static void cache_rstr(FILE * fp, char *s) {
INTsys i = 0; INTsys i;
char buff[256 + 4]; char buff[256 + 4];
linput(fp, buff, 256); linput(fp, buff, 256);
/* an unmatched sscanf leaves i untouched: length 0, not a stack value */ sscanf(buff, INTsysP, &i);
if (sscanf(buff, INTsysP, &i) != 1 || i < 0 || i > 32768) if (i < 0 || i > 32768) /* error, something nasty happened */
i = 0; i = 0;
if (i > 0) { if (i > 0) {
/* store at most s_size-1 bytes, but consume all i bytes so the next field if ((int) fread(s, 1, i, fp) != i) {
stays aligned when a tampered cache declares more than the destination
holds */
const size_t want = (size_t) i;
const size_t store = want < s_size ? want : s_size - 1;
if (fread(s, 1, store, fp) != store) {
assertf(! "fread_cache_failed"); assertf(! "fread_cache_failed");
} }
if (want > store && fseek(fp, (long) (want - store), SEEK_CUR) != 0) {
assertf(!"fseek_cache_failed");
}
s[store] = '\0';
} else {
s[0] = '\0';
} }
*(s + i) = '\0';
} }
static char *cache_rstr_addr(FILE * fp) { static char *cache_rstr_addr(FILE * fp) {
INTsys i = 0; INTsys i;
char *addr = NULL; char *addr = NULL;
char buff[256 + 4]; char buff[256 + 4];
linput(fp, buff, 256); linput(fp, buff, 256);
/* an unmatched sscanf leaves i untouched: length 0, not a stack value */ sscanf(buff, "%d", &i);
if (sscanf(buff, INTsysP, &i) != 1 || i < 0 || i > 32768) if (i < 0 || i > 32768) /* error, something nasty happened */
i = 0; i = 0;
if (i > 0) { if (i > 0) {
addr = malloc(i + 1); addr = malloc(i + 1);
@@ -1432,31 +1400,19 @@ static char *cache_rstr_addr(FILE * fp) {
return addr; return addr;
} }
/* Allocate n+1 bytes for an n-byte cache body, rejecting a hostile/corrupt
length that would demand an absurd allocation or wrap n+1. NULL on refuse. */
static char *cache_alloc_body(size_t n) {
if (n >= (size_t) INT32_MAX)
return NULL;
return (char *) malloc(n + 1);
}
static void cache_rint(FILE * fp, int *i) { static void cache_rint(FILE * fp, int *i) {
char s[256]; char s[256];
cache_rstr(fp, s, sizeof(s)); cache_rstr(fp, s);
if (sscanf(s, "%d", i) != 1) sscanf(s, "%d", i);
*i = 0;
} }
static void cache_rLLint(FILE * fp, unsigned long *i) { static void cache_rLLint(FILE * fp, unsigned long *i) {
int l; int l;
char s[256]; char s[256];
cache_rstr(fp, s, sizeof(s)); cache_rstr(fp, s);
/* reject a non-numeric or negative field: a negative length cast to unsigned sscanf(s, "%d", &l);
would wrap a later malloc(size+1) to a tiny buffer */
if (sscanf(s, "%d", &l) != 1 || l < 0)
l = 0;
*i = (unsigned long) l; *i = (unsigned long) l;
} }
@@ -1516,12 +1472,12 @@ static int PT_LoadCache__Old(PT_Index index_, const char *filename) {
char *a = use; char *a = use;
use[ndxSize] = '\0'; use[ndxSize] = '\0';
a += cache_brstr(a, firstline, sizeof(firstline)); a += cache_brstr(a, firstline);
if (strncmp(firstline, "CACHE-", 6) == 0) { // Nouvelle version du cache if (strncmp(firstline, "CACHE-", 6) == 0) { // Nouvelle version du cache
if (strncmp(firstline, "CACHE-1.", 8) == 0) { // Version 1.1x if (strncmp(firstline, "CACHE-1.", 8) == 0) { // Version 1.1x
cache->version = (int) (firstline[8] - '0'); // cache 1.x cache->version = (int) (firstline[8] - '0'); // cache 1.x
if (cache->version <= 5) { if (cache->version <= 5) {
a += cache_brstr(a, firstline, sizeof(firstline)); a += cache_brstr(a, firstline);
strcpy(cache->lastmodified, firstline); strcpy(cache->lastmodified, firstline);
} else { } else {
fclose(cache->dat); fclose(cache->dat);
@@ -1567,12 +1523,11 @@ static int PT_LoadCache__Old(PT_Index index_, const char *filename) {
if (!firstSeen) { if (!firstSeen) {
if (strstr(line, "/robots.txt") == NULL) { if (strstr(line, "/robots.txt") == NULL) {
PT_Index__Old index = cache; PT_Index__Old index = cache;
const char *scheme =
link_has_authority(line) ? "" : "http://";
firstSeen = 1; firstSeen = 1;
snprintf(index->startUrl, sizeof(index->startUrl), "%s%s", if (!link_has_authority(line))
scheme, line); strcat(index->startUrl, "http://");
strcat(index->startUrl, line);
} }
} }
@@ -1668,13 +1623,9 @@ static PT_Element PT_ReadCache__Old_u(PT_Index index_, const char *url,
String urlDecoded; String urlDecoded;
r->statuscode = old_r.statuscode; r->statuscode = old_r.statuscode;
r->size = old_r.size > 0 ? (size_t) old_r.size : 0; // taille fichier r->size = old_r.size; // taille fichier
/* these fixed fields come straight from fread and carry no guaranteed strcpy(r->msg, old_r.msg);
NUL; terminate them before the bounded copy */ strcpy(r->contenttype, old_r.contenttype);
old_r.msg[sizeof(old_r.msg) - 1] = '\0';
old_r.contenttype[sizeof(old_r.contenttype) - 1] = '\0';
strcpybuff(r->msg, old_r.msg);
strcpybuff(r->contenttype, old_r.contenttype);
/* Guess the destination filename.. this sucks, because this method is not reliable. /* Guess the destination filename.. this sucks, because this method is not reliable.
Yes, the old 1.0 cache format was *that* bogus. /rx */ Yes, the old 1.0 cache format was *that* bogus. /rx */
@@ -1715,27 +1666,26 @@ static PT_Element PT_ReadCache__Old_u(PT_Index index_, const char *url,
cache_rint(cache->dat, &r->statuscode); cache_rint(cache->dat, &r->statuscode);
cache_rLLint(cache->dat, &size_); cache_rLLint(cache->dat, &size_);
r->size = (size_t) size_; r->size = (size_t) size_;
cache_rstr(cache->dat, r->msg, sizeof(r->msg)); cache_rstr(cache->dat, r->msg);
cache_rstr(cache->dat, r->contenttype, sizeof(r->contenttype)); cache_rstr(cache->dat, r->contenttype);
if (cache->version >= 3) if (cache->version >= 3)
cache_rstr(cache->dat, r->charset, sizeof(r->charset)); cache_rstr(cache->dat, r->charset);
cache_rstr(cache->dat, r->lastmodified, sizeof(r->lastmodified)); cache_rstr(cache->dat, r->lastmodified);
cache_rstr(cache->dat, r->etag, sizeof(r->etag)); cache_rstr(cache->dat, r->etag);
cache_rstr(cache->dat, r->location, sizeof(location_default)); cache_rstr(cache->dat, r->location);
if (cache->version >= 2) if (cache->version >= 2)
cache_rstr(cache->dat, r->cdispo, sizeof(r->cdispo)); cache_rstr(cache->dat, r->cdispo);
if (cache->version >= 4) { if (cache->version >= 4) {
cache_rstr(cache->dat, previous_save_, sizeof(previous_save_)); // adr cache_rstr(cache->dat, previous_save_); // adr
cache_rstr(cache->dat, previous_save_, sizeof(previous_save_)); // fil cache_rstr(cache->dat, previous_save_); // fil
previous_save[0] = '\0'; previous_save[0] = '\0';
cache_rstr(cache->dat, previous_save_, cache_rstr(cache->dat, previous_save_); // save
sizeof(previous_save_)); // save
} }
if (cache->version >= 5) { if (cache->version >= 5) {
r->headers = cache_rstr_addr(cache->dat); r->headers = cache_rstr_addr(cache->dat);
} }
// //
cache_rstr(cache->dat, check, sizeof(check)); cache_rstr(cache->dat, check);
if (strcmp(check, "HTS") == 0) { /* intégrité OK */ if (strcmp(check, "HTS") == 0) { /* intégrité OK */
ok = 1; ok = 1;
} }
@@ -1764,8 +1714,7 @@ static PT_Element PT_ReadCache__Old_u(PT_Index index_, const char *url,
&& (!isalpha(previous_save_[0]) || previous_save_[1] != ':')) // c:/home/foo/bar.gif && (!isalpha(previous_save_[0]) || previous_save_[1] != ':')) // c:/home/foo/bar.gif
) { ) {
index->safeCache = 1; index->safeCache = 1;
snprintf(previous_save, sizeof(previous_save), "%s%s", index->path, sprintf(previous_save, "%s%s", index->path, previous_save_);
previous_save_);
} }
// bogus format (includes buggy absolute path) // bogus format (includes buggy absolute path)
else { else {
@@ -1790,16 +1739,15 @@ static PT_Element PT_ReadCache__Old_u(PT_Index index_, const char *url,
int saveLen = (int) strlen(previous_save_); int saveLen = (int) strlen(previous_save_);
if (index->fixedPath < saveLen) { if (index->fixedPath < saveLen) {
snprintf(previous_save, sizeof(previous_save), "%s%s", sprintf(previous_save, "%s%s", index->path,
index->path, previous_save_ + index->fixedPath); previous_save_ + index->fixedPath);
} else { } else {
snprintf(r->msg, sizeof(r->msg), "Bogus fixePath prefix for %s (prefixLen=%d)", snprintf(r->msg, sizeof(r->msg), "Bogus fixePath prefix for %s (prefixLen=%d)",
previous_save_, (int) index->fixedPath); previous_save_, (int) index->fixedPath);
r->statuscode = STATUSCODE_INVALID; r->statuscode = STATUSCODE_INVALID;
} }
} else { } else {
snprintf(previous_save, sizeof(previous_save), "%s%s", sprintf(previous_save, "%s%s", index->path, previous_save_);
index->path, previous_save_);
} }
} }
} }
@@ -1816,7 +1764,7 @@ static PT_Element PT_ReadCache__Old_u(PT_Index index_, const char *url,
FILE *fp = fopen(previous_save, "rb"); FILE *fp = fopen(previous_save, "rb");
if (fp != NULL) { if (fp != NULL) {
r->adr = cache_alloc_body(r->size); r->adr = (char *) malloc(r->size + 1);
if (r->adr != NULL) { if (r->adr != NULL) {
if (r->size > 0 && fread(r->adr, 1, r->size, fp) != r->size) { if (r->size > 0 && fread(r->adr, 1, r->size, fp) != r->size) {
r->statuscode = STATUSCODE_INVALID; r->statuscode = STATUSCODE_INVALID;
@@ -1836,7 +1784,7 @@ static PT_Element PT_ReadCache__Old_u(PT_Index index_, const char *url,
} else { } else {
// lire fichier (d'un coup) // lire fichier (d'un coup)
if (flags & FETCH_BODY) { if (flags & FETCH_BODY) {
r->adr = cache_alloc_body(r->size); r->adr = (char *) malloc(r->size + 1);
if (r->adr != NULL) { if (r->adr != NULL) {
if (fread(r->adr, 1, r->size, cache->dat) != r->size) { // erreur if (fread(r->adr, 1, r->size, cache->dat) != r->size) { // erreur
free(r->adr); free(r->adr);

View File

@@ -23,7 +23,7 @@ conv 'iso-8859-1' 'café' 'café'
# windows-1252 is NOT latin-1: 0x80 is the euro sign, not U+0080. This is the # windows-1252 is NOT latin-1: 0x80 is the euro sign, not U+0080. This is the
# case that actually exercises the cp1252 table (the 0x80-0x9F range). # case that actually exercises the cp1252 table (the 0x80-0x9F range).
conv 'windows-1252' 'hex:80' '€' conv 'windows-1252' $'\x80' '€'
# pure ASCII is charset-invariant # pure ASCII is charset-invariant
conv 'us-ascii' 'hello' 'hello' conv 'us-ascii' 'hello' 'hello'
@@ -34,8 +34,8 @@ conv 'no-such-charset-xyz' 'abc' 'abc'
test "$(httrack -O /dev/null -#test=charset 'no-such-charset-xyz' 'café' 2>/dev/null)" == "" || exit 1 test "$(httrack -O /dev/null -#test=charset 'no-such-charset-xyz' 'café' 2>/dev/null)" == "" || exit 1
# malformed UTF-8 (lone continuation byte, truncated lead byte) must not crash # malformed UTF-8 (lone continuation byte, truncated lead byte) must not crash
runs 'utf-8' 'hex:80' runs 'utf-8' $'\x80'
runs 'utf-8' 'hex:c3' runs 'utf-8' $'\xc3'
# hts_getCharsetFromMeta: <meta> charset extraction, HTML5 and legacy forms. # hts_getCharsetFromMeta: <meta> charset extraction, HTML5 and legacy forms.
# -#test=metacharset <html> prints the charset, or "(none)". # -#test=metacharset <html> prints the charset, or "(none)".
@@ -89,11 +89,11 @@ utf8() {
utf8 'plain' '1' utf8 'plain' '1'
utf8 'café' '1' utf8 'café' '1'
utf8 '统计' '1' utf8 '统计' '1'
utf8 'hex:636166e9' '0' # latin-1 accent utf8 "$(printf 'caf\xe9')" '0' # latin-1 accent
utf8 'hex:c3' '0' # truncated lead byte utf8 "$(printf '\xc3')" '0' # truncated lead byte
utf8 'hex:a9' '0' # lone continuation byte utf8 "$(printf '\xa9')" '0' # lone continuation byte
utf8 'hex:f09f9880' '1' # 4-byte emoji utf8 "$(printf '\xf0\x9f\x98\x80')" '1' # 4-byte emoji
utf8 'hex:c0af' '0' # overlong (latin-1 "À¯" must stay convertible) utf8 "$(printf '\xc0\xaf')" '0' # overlong (latin-1 "À¯" must stay convertible)
utf8 'hex:eda080' '0' # UTF-16 surrogate utf8 "$(printf '\xed\xa0\x80')" '0' # UTF-16 surrogate
utf8 'hex:f4908080' '0' # above U+10FFFF utf8 "$(printf '\xf4\x90\x80\x80')" '0' # above U+10FFFF
utf8 'hex:f888808080' '0' # legacy 5-byte form utf8 "$(printf '\xf8\x88\x80\x80\x80')" '0' # legacy 5-byte form

View File

@@ -52,15 +52,14 @@ run "$tmp/dir" "$tmp/adir"
loghas 'Could not include URL list "[^"]+": not a regular file' loghas 'Could not include URL list "[^"]+": not a regular file'
# unreadable regular file: the fopen() errno arm fires, distinct from the # unreadable regular file: the fopen() errno arm fires, distinct from the
# directory branch. Needs an OS that enforces mode 000: root bypasses it, and # directory branch. Root bypasses mode 000, so skip it there.
# Windows ignores chmod entirely, so probe rather than assume. if test "$(id -u)" -ne 0; then
: >"$tmp/noperm.txt" : >"$tmp/noperm.txt"
chmod 000 "$tmp/noperm.txt" chmod 000 "$tmp/noperm.txt"
if ! cat "$tmp/noperm.txt" >/dev/null 2>&1; then
run "$tmp/perm" "$tmp/noperm.txt" run "$tmp/perm" "$tmp/noperm.txt"
chmod 644 "$tmp/noperm.txt"
loghas 'Could not include URL list "[^"]+": .+' loghas 'Could not include URL list "[^"]+": .+'
lognot 'not a regular file' lognot 'not a regular file'
fi fi
chmod 644 "$tmp/noperm.txt"
exit 0 exit 0

View File

@@ -193,13 +193,5 @@ test "$(with_timeout httrack -O /dev/null -#test=filter "$pat" "$subj")" == "$su
match '*[aX]X*[a]Y' 'aXaXaY' match '*[aX]X*[a]Y' 'aXaXaY'
nomatch '*[aX]X*[a]Y' 'aXbXaY' nomatch '*[aX]X*[a]Y' 'aXbXaY'
# hostile patterns must not stack-overflow or hang: length + depth + work caps # hostile patterns must not stack-overflow or hang: length + work caps
test "$(with_timeout httrack -O /dev/null -#test=filterbounds)" == "filterbounds: OK" || exit 1 test "$(with_timeout httrack -O /dev/null -#test=filterbounds)" == "filterbounds: OK" || exit 1
# and the depth cap must hold on the 1MB stack a Windows thread gets (#574):
# uncapped, the same pattern recursed ~900KB deep. Skipped where 512K is too
# small to even start the binary.
if (ulimit -s 512 && httrack --version >/dev/null 2>&1); then
out=$(ulimit -s 512 && with_timeout httrack -O /dev/null -#test=filterbounds)
test "$out" == "filterbounds: OK" || exit 1
fi

View File

@@ -1,18 +0,0 @@
#!/bin/bash
#
# File sizes past the 32-bit wrap ('httrack -#test=fsize <dir>'). 5GB, so a
# signed *and* an unsigned truncation both show up; sparse, so it costs no disk.
set -euo pipefail
dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT
out=$(httrack -#test=fsize "$dir")
echo "$out"
want="fsize: width=8,8 size=5368709120,5368709120 psize=5368709120 absent=-1"
test "$out" == "$want" || {
echo "FAIL: unexpected size report (want '$want')"
exit 1
}

View File

@@ -38,7 +38,4 @@ dec 'xn--!!!' '㚯'
dec 'xn--already-encoded.com' 'ǮalǭǮreaǭǫdy.com' dec 'xn--already-encoded.com' 'ǮalǭǮreaǭǫdy.com'
# second label has a truncated UTF-8 sequence: used to leak the segment buffer. # second label has a truncated UTF-8 sequence: used to leak the segment buffer.
# Fed as hex: the raw bytes must reach the encoder intact, but invalid UTF-8 enc_bad "$(printf 'b\xc3\xbcchev.\xe4\xbe\x8b\xe5\xad\x62\xc3\xbccheple')"
# cannot survive a Windows command line (it round-trips through UTF-16 to valid
# U+FFFD, which then encodes fine instead of being rejected).
enc_bad 'hex:62c3bc636865762ee4be8be5ad62c3bc636865706c65'

View File

@@ -32,13 +32,6 @@ esac
tmp=$(mktemp -d "${TMPDIR:-/tmp}/httrack_rcfile.XXXXXX") || exit 1 tmp=$(mktemp -d "${TMPDIR:-/tmp}/httrack_rcfile.XXXXXX") || exit 1
trap 'rm -rf "$tmp"' EXIT HUP INT QUIT PIPE TERM trap 'rm -rf "$tmp"' EXIT HUP INT QUIT PIPE TERM
# HTS_HTTRACKRC is ".httrackrc" on POSIX but "httrackrc" on Windows: write both,
# each platform reads the one it knows.
write_rc() {
cat >"$1/.httrackrc"
cp "$1/.httrackrc" "$1/httrackrc"
}
# --- 1. alias token survives the bound intact ------------------------------- # --- 1. alias token survives the bound intact -------------------------------
d1="$tmp/intact" d1="$tmp/intact"
mkdir -p "$d1" mkdir -p "$d1"
@@ -47,7 +40,7 @@ echo '<html><body>hello</body></html>' >"$d1/index.html"
# optinclude_file() lowercases each config line, so the marker is lowercase to # optinclude_file() lowercases each config line, so the marker is lowercase to
# survive the comparison verbatim. # survive the comparison verbatim.
marker='zzz_rcfile_marker_0123456789_abcdefghijklmnopqrstuvwxyz_intact' marker='zzz_rcfile_marker_0123456789_abcdefghijklmnopqrstuvwxyz_intact'
printf 'user-agent=%s\n' "$marker" | write_rc "$d1" printf 'user-agent=%s\n' "$marker" >"$d1/.httrackrc"
# Run with no -O so the working-directory .httrackrc is loaded (an -O path makes # Run with no -O so the working-directory .httrackrc is loaded (an -O path makes
# the engine skip the rc files). Output lands in the temp dir. Guard the run so a # the engine skip the rc files). Output lands in the temp dir. Guard the run so a
@@ -80,7 +73,7 @@ echo '<html><body>hi</body></html>' >"$d2/index.html"
val=$(printf 'a%.0s' $(seq 1 200)) val=$(printf 'a%.0s' $(seq 1 200))
for _ in $(seq 1 400); do for _ in $(seq 1 400); do
printf 'user-agent=%s\n' "$val" printf 'user-agent=%s\n' "$val"
done | write_rc "$d2" done >"$d2/.httrackrc"
# The process aborts (httrack turns the fatal signal into exit 134 either way), # The process aborts (httrack turns the fatal signal into exit 134 either way),
# so the exit code does not distinguish the bounded abort from a heap overflow; # so the exit code does not distinguish the bounded abort from a heap overflow;

View File

@@ -32,10 +32,10 @@ case "$err" in
esac esac
# Same guarantee for the htsbuff builder. The source is exactly the buffer # Same guarantee for the htsbuff builder. The source is exactly the buffer
# capacity (6 bytes into a 6-byte buffer), so this also pins the boundary: a # capacity (4 bytes into a 4-byte buffer), so this also pins the boundary: a
# '<=' off-by-one in the capacity check would let it through (and print "NOT # '<=' off-by-one in the capacity check would let it through (and print "NOT
# aborted"). Match the specific htsbuff abort message, not just any assert. # aborted"). Match the specific htsbuff abort message, not just any assert.
err=$(httrack -#test=strsafe overflow-buff "abcdef" 2>&1) || true err=$(httrack -#test=strsafe overflow-buff "abcd" 2>&1) || true
case "$err" in case "$err" in
*"strsafe: NOT aborted"*) *"strsafe: NOT aborted"*)
echo "htsbuff over-capacity write was NOT caught" >&2 echo "htsbuff over-capacity write was NOT caught" >&2

View File

@@ -1,21 +0,0 @@
#!/bin/bash
# A corrupt proxytrack .ndx must not overflow the fixed cache-read buffers. The
# sanitizer CI build turns any regression here into a hard stack/heap overflow.
set -euo pipefail
dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT
# The first length-prefixed field declares far more than firstline[256] holds;
# cache_brstr must clamp the copy to the destination, not the declared length.
{
printf '4000\n'
head -c 4000 /dev/zero | tr '\0' 'A'
} >"$dir/foo.ndx"
: >"$dir/foo.dat" # cache_brstr runs only when the sibling .dat opens
proxytrack --convert "$dir/out.arc" "$dir/foo.ndx" >/dev/null 2>&1 || {
echo "FAIL: proxytrack crashed/errored on a corrupt cache index"
exit 1
}

View File

@@ -57,7 +57,6 @@ TESTS = \
01_engine-pause.test \ 01_engine-pause.test \
01_engine-rcfile.test \ 01_engine-rcfile.test \
01_engine-reconcile.test \ 01_engine-reconcile.test \
01_engine-fsize.test \
01_engine-redirect.test \ 01_engine-redirect.test \
01_engine-relative.test \ 01_engine-relative.test \
01_engine-robots.test \ 01_engine-robots.test \
@@ -129,7 +128,6 @@ TESTS = \
49_local-cookiewall.test \ 49_local-cookiewall.test \
50_local-contentcodings.test \ 50_local-contentcodings.test \
51_local-update-codec.test \ 51_local-update-codec.test \
52_local-socks5.test \ 52_local-socks5.test
53_local-proxytrack-cache-corrupt.test
CLEANFILES = check-network_sh.cache CLEANFILES = check-network_sh.cache