Skip to content

Keep XML references from truncating WebDAV file paths - #765

Merged
WaylandYang merged 3 commits into
deeplethe:devfrom
Maya-Kid:codex/fix-webdav-xml-href
Sep 19, 2026
Merged

WaylandYang merged 3 commits into
deeplethe:devfrom
Maya-Kid:codex/fix-webdav-xml-href

Conversation

@Maya-Kid

Copy link
Copy Markdown
Contributor

Problem

A WebDAV file named A&B.txt can be silently skipped during synchronization. A valid PROPFIND response represents its path as /docs/A&B.txt. quick-xml emits text, a reference, and more text separately, but the parser overwrites href on every text event and ignores references. The resulting path is only B.txt; the subsequent GET targets /B.txt and a 404 is treated as an unreadable file.

Change

Accumulate text, character/entity references, and CDATA until the field closes, then interpret href, content length, and last-modified values. Percent decoding runs after the full href is assembled. Whitespace is trimmed at field boundaries rather than between XML fragments. Unknown properties remain ignored; no new dependency or API/schema change.

Reproduction and validation

Base: 816d4a39255b68849d9eecaf273ec32b3f7d793d (dev).

  • Parser regression covers &, decimal/hex character references, CDATA, and mixed text/CDATA. On baseline the href is B.txt instead of /docs/A&B.txt.
  • End-to-end regression invokes the real fetch against a local HTTP fixture: the original implementation returns zero files; the fixed implementation downloads the expected file and retains its filename, bytes, and external key.
  • Before: 2 regression failures, 4 existing tests passed. After: all 6 passed.
  • cargo +1.98.1 test -p utopia-server --locked webdav::tests:: -- --skip it_reads_from_a_real_webdav_server
  • Workspace cargo fmt --all --check and cargo clippy --workspace --all-targets --locked -- -D warnings: passed.
  • git diff --check: passed.

The optional externally configured WebDAV server test was explicitly excluded; the local HTTP integration test did run. No existing deployment, credentials, or database were used. Full workspace runtime tests were not repeated for this patch.

Signed-off-by: dada-yan <BinjunYann@gmail.com>

@WaylandYang WaylandYang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bug is real and the tests are good. With the three hunks reverted and your tests kept:

---- webdav::tests::xml_text_fragments_keep_the_complete_href_and_properties stdout ----
assertion `left == right` failed: /docs/A&amp;B.txt
  left: "B.txt"
 right: "/docs/A&B.txt"

---- webdav::tests::an_xml_escaped_filename_is_fetched_from_its_full_path stdout ----
assertion `left == right` failed: the escaped href must not be silently skipped
  left: 0
 right: 1

The second one is the right test to have written: it shows the consequence, which is that the file leaves the sync entirely.

I would not merge this yet, because it drops files through a different door

Flushing on every End whose name matches the open field writes an empty string over a good value when a property is spelled twice. Nextcloud, ownCloud and SabreDAV routinely answer with two propstat blocks, a 200 carrying values and a 404 listing the properties they could not find. I ran the same document against both branches:

PROBE 成对写法   len=0     ← this branch
PROBE 自闭合写法 len=5     ← this branch, <getcontentlength/>
PROBE dev 成对写法 len=5   ← origin/dev

with

<propstat><prop><getcontentlength>5</getcontentlength></prop>
  <status>HTTP/1.1 200 OK</status></propstat>
<propstat><prop><getcontentlength></getcontentlength></prop>
  <status>HTTP/1.1 404 Not Found</status></propstat>

fetch does if e.len == 0 || e.len > MAX_FILE_BYTES { continue; }, so a clobbered length silently drops the file. That is the symptom this pull request exists to prevent, arriving through the other spelling. The self-closing form is safe, because quick-xml gives it as Event::Empty and the field is never opened, so whether a server is hit depends on its serializer.

The fix looks like one line: flush only when the accumulated text is not empty, or track whether a text event was seen for this field. Either keeps your accumulation and stops an empty re-declaration from overwriting.

Two more, smaller

  • unescape(...)? propagates, so one unrecognized entity anywhere aborts the whole listing: PROBE unknown-entity ERR: at 1..5: unrecognized entity 'nbsp', where dev returns the other entries and mangles only that one. Failing loudly may be right, but it is a blast-radius change past the title and no test covers it. Worth a decision either way.
  • field is only assigned in the other => arm, so response and collection Start events leave the previous field and its buffer open. I could not build a failure from it, but the state is not a stack and reads as though it were.

Dropping trim_text(true) is a bigger change than the title suggests, and correctness now rests on the three .trim() calls at flush time. I found no behaviour difference, so this is a note.

Comments in this repo are Chinese; the new ones are English.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: WaylandYang <wayland0916@gmail.com>
@WaylandYang

Copy link
Copy Markdown
Contributor

I pushed the two fixes to your branch rather than asking you to do them, since I had the probe in front of me. Please read the commit and say if you disagree.

  • An empty redeclaration no longer clobbers. The flush now requires the accumulated text to be non-empty, so the 404 propstat that re-lists a property it could not find leaves the 200 block's value alone. A test covers both spellings, because the self-closing one arrives as Event::Empty and never opens the field, so only the paired one was broken.
  • An unrecognized entity stays local. unescape failing now leaves the reference as written instead of propagating, so one &nbsp; in one href no longer aborts the whole PROPFIND. A second test keeps the other entries in the listing.

Your two original tests still pass, along with the other five in the module: 9 green. fmt and clippy clean.

@WaylandYang
WaylandYang merged commit f8b2ebc into deeplethe:dev Sep 19, 2026
@WaylandYang WaylandYang mentioned this pull request Sep 19, 2026
@Maya-Kid
Maya-Kid deleted the codex/fix-webdav-xml-href branch September 26, 2026 05:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants