diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml index 418cdc11..f575e157 100644 --- a/.github/workflows/windows-build.yml +++ b/.github/workflows/windows-build.yml @@ -262,11 +262,11 @@ jobs: # tested nothing: pin the skips, and floor the passes in case the glob empties. # footer-overflow and purge-longpath skip on Windows (need a path past MAX_PATH); # crange pending #581; - # webdav-mime needs a reapable background listener, which MSYS cannot give it; + # webdav-default and webdav-mime need a reapable background listener, which MSYS cannot give them; # badmtime needs a filesystem that stores an mtime past gmtime's range; # single-file ends on a GUI half needing htsserver, which this job does not build; # update-304-leak needs a LeakSanitizer build, which MSVC has no equivalent of. - expected_skips=" 01_engine-footer-overflow.test 100_local-purge-longpath.test 114_local-update-304-leak.test 48_local-crange-memresume.test 71_local-crange-repaircache.test 79_local-proxytrack-webdav-mime.test 88_local-proxytrack-badmtime.test 94_local-single-file.test" + expected_skips=" 01_engine-footer-overflow.test 100_local-purge-longpath.test 114_local-update-304-leak.test 120_local-proxytrack-webdav-default.test 48_local-crange-memresume.test 71_local-crange-repaircache.test 79_local-proxytrack-webdav-mime.test 88_local-proxytrack-badmtime.test 94_local-single-file.test" # First, or the deadline reads as an unexplained shortfall in the gates below. [ "$deadline" -eq 0 ] || { echo "::error::suite did not finish within ${suite_deadline}s"; exit 1; } [ "$pass" -ge 90 ] || { echo "::error::only $pass tests passed ($skip skipped)"; exit 1; } diff --git a/src/proxy/proxytrack.c b/src/proxy/proxytrack.c index c34afbf8..e74feddb 100644 --- a/src/proxy/proxytrack.c +++ b/src/proxy/proxytrack.c @@ -748,7 +748,8 @@ static PT_Element proxytrack_process_DAV_Request(PT_Indexes indexes, const char *thisUrl = list[i]; const char *mimeType = "application/octet-stream"; unsigned int thisUrlLen = (unsigned int) strlen(thisUrl); - int thisIsDir = (thisUrl[thisUrlLen - 1] == '/') ? 1 : 0; + /* the folder's default document is enumerated as an empty name */ + int thisIsDir = (hts_lastchar(thisUrl) == '/') ? 1 : 0; /* Item URL */ StringRoom(itemUrl, @@ -853,8 +854,7 @@ static PT_Element proxytrack_process_HTTP_List(PT_Indexes indexes, for(isDir = 1; isDir >= 0; isDir--) { for(i = 0; list[i] != NULL; i++) { char *thisUrl = list[i]; - unsigned int thisUrlLen = (unsigned int) strlen(thisUrl); - int thisIsDir = (thisUrl[thisUrlLen - 1] == '/') ? 1 : 0; + int thisIsDir = (hts_lastchar(thisUrl) == '/') ? 1 : 0; if (thisIsDir == isDir) { if (isDir) diff --git a/tests/120_local-proxytrack-webdav-default.test b/tests/120_local-proxytrack-webdav-default.test new file mode 100755 index 00000000..f36f9791 --- /dev/null +++ b/tests/120_local-proxytrack-webdav-default.test @@ -0,0 +1,121 @@ +#!/bin/bash +# +# A PROPFIND whose path is an exact cache entry enumerates that entry's default +# document under an empty name; proxytrack used to index one byte before it and +# die on the whole listener. + +set -euo pipefail + +: "${top_srcdir:=..}" +# shellcheck source=tests/testlib.sh +. "$top_srcdir/tests/testlib.sh" + +python=$(find_python) || { + echo "python3 missing, skipping" + exit 77 +} +command -v curl >/dev/null 2>&1 || { + echo "curl missing, skipping" + exit 77 +} +# MSYS cannot reap a native listener, and the orphan wedges the suite (#595). +if is_windows; then + echo "windows: cannot reap a backgrounded proxytrack, skipping" + exit 77 +fi + +dir=$(mktemp -d) +ptpid= +cleanup() { + stop_server "$ptpid" + rm -rf "$dir" +} +trap 'set +e; cleanup' EXIT +trap 'set +e; cleanup; exit 1' INT TERM HUP + +printf 'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 5\r\n\r\n' >"$dir/hdr" +printf 'hello' >"$dir/body" +alen=$(($(wc -c <"$dir/hdr") + $(wc -c <"$dir/body"))) +{ + printf 'filedesc://t.arc 0.0.0.0 20250101000000 text/plain 200 - - 0 t.arc 9\n' + printf '2 0 test\n' + printf '\n\n' + printf 'http://example.com/page.html 0.0.0.0 20250101000000 text/html 200 - - 0 t.arc %d\n' "$alen" + cat "$dir/hdr" "$dir/body" +} >"$dir/in.arc" + +freeport() { + "$python" -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close()' +} +proxyport=$(freeport) +icpport=$(freeport) + +proxytrack "127.0.0.1:$proxyport" "127.0.0.1:$icpport" "$dir/in.arc" >"$dir/pt.log" 2>&1 & +ptpid=$! +waited=0 +until grep -qE "HTTP Proxy installed on|Unable to (initialize a temporary server|create the server)" "$dir/pt.log"; do + kill -0 "$ptpid" 2>/dev/null || { + echo "FAIL: proxytrack exited before listening" + cat "$dir/pt.log" + exit 1 + } + test "$waited" -lt 50 || { + echo "FAIL: proxytrack never announced its listen port" + exit 1 + } + sleep 0.1 + waited=$((waited + 1)) +done +grep -q "HTTP Proxy installed on" "$dir/pt.log" || { + echo "FAIL: proxytrack failed to bind" + cat "$dir/pt.log" + exit 1 +} + +propfind() { + curl -s -i --max-time 30 -X PROPFIND -H "Depth: 1" \ + "http://127.0.0.1:$proxyport/webdav/example.com/$1" +} + +resp=$(propfind page.html) || { + echo "FAIL: PROPFIND on an exact cache entry did not answer" + cat "$dir/pt.log" + exit 1 +} +grep -q '^HTTP/1\.[01] 207 ' <<<"$resp" || { + echo "FAIL: expected 207 Multi-Status" + printf '%s\n' "$resp" + exit 1 +} +grep -q 'Default Document for the Folder' <<<"$resp" || { + echo "FAIL: the default document of the entry was not listed" + printf '%s\n' "$resp" + exit 1 +} +# The name alone would still pass on a truncated or garbage URL, so pin the +# href: the default document is the collection path plus an empty name. +grep -q '/webdav/example\.com/page\.html/' <<<"$resp" || { + echo "FAIL: the default document was listed under the wrong href" + printf '%s\n' "$resp" + exit 1 +} +responses=$(grep -c '