Compare commits

...

2 Commits

Author SHA1 Message Date
Xavier Roche
1e0def63ea Merge origin/master into fix-623-delayed-marker
Resolve tests/Makefile.am: keep both 66_engine-port80-strip (from #632) and
67_engine-delayed-truncate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-18 07:58:59 +02:00
Xavier Roche
2acc796fbe Keep the .delayed placeholder marker when the path ceiling truncates the name
url_savename appends the mandatory ".delayed" placeholder extension before it
enforces the 236-char path ceiling, and the ceiling cuts the tail of the last
segment, where the marker lives. A cut marker fails IS_DELAYED_EXT, so
back_delayed_rename never renames the file to its final name and the download
is lost. Reachable via #133-style deep paths ending in a long hashed segment.

Reserve the trailing ".<id>.delayed" in the last-segment copy so it survives
the cut, trimming the head of the segment instead. The hex-id absorb stops at
its dot separator, so an all-hex hashed base is never mistaken for the tag.

Closes #623

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
2026-07-18 07:26:04 +02:00
3 changed files with 98 additions and 3 deletions

View File

@@ -1532,8 +1532,30 @@ int url_savename(lien_adrfilsave *const afs,
// last segment
wsave[j++] = '/';
#define MAX_UTF8_SEQ_CHARS 4
for(i = lastSeg; wsave[i] != '\0' && j < maxLen; i++) {
wsave[j++] = wsave[i];
{
// #623: the ".delayed" placeholder marker sits at the tail; cutting
// through it drops IS_DELAYED_EXT, so the file is never renamed to its
// final name. Reserve the trailing ".<id>.delayed" across the cut.
size_t markStart = wsaveLen;
if (IS_DELAYED_EXT(afs->save)) {
const size_t extDot = wsaveLen - strlen("." DELAYED_EXT);
size_t p = extDot; /* walk back over a dot-separated ".<hexid>" tag */
while (p > lastSeg && ((wsave[p - 1] >= '0' && wsave[p - 1] <= '9') ||
(wsave[p - 1] >= 'a' && wsave[p - 1] <= 'f')))
p--;
// keep the tag only if truly ".<hexid>.delayed"; else the bare marker
// (a wholly-hex base, e.g. a hashed #133 name, must not be absorbed)
markStart = (p > lastSeg && p < extDot && wsave[p - 1] == '.')
? p - 1
: extDot;
}
// head, bounded so the marker still fits, then the marker itself
for (i = lastSeg; i < markStart && j + (wsaveLen - markStart) < maxLen;
i++)
wsave[j++] = wsave[i];
for (i = markStart; i < wsaveLen && j < maxLen; i++)
wsave[j++] = wsave[i];
}
// terminating \0
wsave[j++] = '\0';

View File

@@ -0,0 +1,72 @@
#!/bin/bash
#
set -euo pipefail
# #623: url_savename enforces the 236-char path ceiling by cutting the tail of
# the last segment, where the mandatory ".delayed" placeholder marker lives. A
# cut marker fails IS_DELAYED_EXT, so back_delayed_rename never renames the file
# to its final name and the download is lost. The marker must survive the cut.
# statuscode=302 status=-1 = a redirect answer still downloading: no type is
# resolved, so the name gets a ".<id>.delayed" placeholder (see 01_engine-savename).
CEIL=236
httrack_bin=$(cd "$(dirname "$(command -v httrack)")" && pwd)/httrack
scratch=$(mktemp -d)
trap 'rm -rf "$scratch"' EXIT
cd "$scratch"
run() {
"$httrack_bin" -O /dev/null -#test=savename "$@" | sed -n 's/^savename: //p'
}
# A deep path ending in a long segment that overruns the ceiling.
deep="/d1$(printf 'a%.0s' {1..40})/d2$(printf 'b%.0s' {1..40})"
deep="$deep/d3$(printf 'c%.0s' {1..40})/d4$(printf 'd%.0s' {1..40})"
long="$deep/$(printf 'z%.0s' {1..90})"
out="$(run "$long" text/html statuscode=302 status=-1)"
case "$out" in
*.delayed) ;;
*)
echo "FAIL: delayed marker cut by truncation: '$out'"
exit 1
;;
esac
test "${#out}" -le "$CEIL" || {
echo "FAIL: truncated name ${#out} > $CEIL ceiling: '$out'"
exit 1
}
# #133-style hashed name: an all-hex last segment must keep the marker too (the
# ".<id>." tag is not mistaken for part of the hash).
hexseg=$(printf 'a1b2c3d4e5f60718%.0s' {1..8})
hexpath="/d1$(printf 'x%.0s' {1..30})/d2$(printf 'y%.0s' {1..30})/$hexseg"
out="$(run "$hexpath" text/html statuscode=302 status=-1)"
case "$out" in
*.delayed) ;;
*)
echo "FAIL: hashed-name marker cut by truncation: '$out'"
exit 1
;;
esac
test "${#out}" -le "$CEIL" || {
echo "FAIL: hashed name ${#out} > $CEIL ceiling: '$out'"
exit 1
}
# A non-delayed name of the same shape still truncates, with no marker to keep.
out="$(run "$long.html" text/html)"
test "${#out}" -le "$CEIL" || {
echo "FAIL: non-delayed name ${#out} > $CEIL ceiling: '$out'"
exit 1
}
case "$out" in
*.delayed)
echo "FAIL: non-delayed name grew a .delayed marker: '$out'"
exit 1
;;
esac
echo "delayed-truncate OK"

View File

@@ -147,6 +147,7 @@ TESTS = \
63_webhttrack-home.test \
64_local-intl-outdir.test \
65_port-siblings.test \
66_engine-port80-strip.test
66_engine-port80-strip.test \
67_engine-delayed-truncate.test
CLEANFILES = check-network_sh.cache