Skip to content

Drop malformed attributes instead of trapping on them (fixes #392) - #400

Merged
aehlke merged 1 commit into
scinfu:masterfrom
Shimmur:drop-malformed-attributes
Jun 19, 2026
Merged

Drop malformed attributes instead of trapping on them (fixes #392)#400
aehlke merged 1 commit into
scinfu:masterfrom
Shimmur:drop-malformed-attributes

Conversation

@lukeredpath

Copy link
Copy Markdown
Contributor

Fixes #392. That issue reported a try! crash in attribute materialization but was closed pending a reproducer — here is a minimal public-API one, plus a fix that removes the try! traps entirely.

Reproducer (stock 2.13.x)

let doc = try SwiftSoup.parse("<div a=\"b\"\u{0B}>hi</div>")
_ = try doc.select("[name=x]")
// Attributes.swift: Fatal error: 'try!' expression unexpectedly raised an error:
//   IllegalArgumentException, Message: "String must not be empty"

A vertical tab (0x0B) immediately after a quoted attribute value is the trigger.

Root cause

Attributes are parsed lazily into a deferred store and only materialized on first access (size(), getIgnoreCase(), the hot-attribute query-index rebuild during select(), …). A key can be non-empty as raw bytes (passing every parse-time !isEmpty guard) yet become empty after ByteSlice.trim(): ByteSlice.trim() strips 0x09…0x0D + 0x20 — this range includes 0x0B — but the HTML5 tokeniser's attribute-name delimiter set (CharacterReader.attributeNameDelims) is {\t \n \r \f space / = > NUL " ' <}, which does not include 0x0B, so 0x0B is read as a normal attribute-name character.

A 0x0B-only name therefore survives parsing, then trims to empty when materialized. appendPending/ensureMaterialized feed that empty key to the throwing Attribute/BooleanAttribute initialisers via try!, turning a recoverable validation error into a fatal, uncatchable trap.

Why only after a quoted value? Every other whitespace skip uses advanceAsciiWhitespace(), whose UInt8.isWhitespace predicate does include 0x0B, so a stray 0x0B is swallowed. Only AfterAttributeValue_quoted errors and transitions to BeforeAttributeName without advancing, leaving 0x0B to be consumed as a name by the explicit five-byte whitespace check that excludes it.

Fix

Rather than guarding the one known-bad input, this removes the try! traps from attribute materialization altogether. A throwing helper (makeMaterializedAttribute) builds the attribute, and the three call sites (appendPending, ensureMaterialized, and the single-attribute fallback) invoke it via try?, dropping any attribute that fails validation. This matches jsoup (which silently drops malformed empty-key attributes) and makes materialization resilient to any validation failure, not just the empty-key case — a third-party parser should never try! on uncontrolled input.

Tests / verification

  • Adds Tests/SwiftSoupTests/PublicEmptyAttributeKeyTest.swift — boolean, valued, and <meta> selector cases, public API only. Traps before the fix; after, select returns 0 matches (attribute dropped).
  • Full suite: 662 tests, 0 failures.
  • Benchmark (BenchmarkProfileTest, release, SET=base,large, 500 iterations, same machine): baseline f474b11 32586.93 ms vs fixed d33de7b 32324.84 ms (−0.80%) — within run-to-run noise; the helper is @inline(__always), so codegen at the call sites is unchanged.

Attribute materialization used try\! on the throwing Attribute/BooleanAttribute
initialisers, so any key that fails validation crashes the process fatally and
uncatchably. The reproducible case: a vertical tab (0x0B) consumed as an
attribute name survives parsing (the tokeniser's attribute-name delimiter set
excludes 0x0B) but trims to empty in ByteSlice.trim() (which strips 0x09–0x0D +
0x20), reaching the initialiser as an empty key and trapping with
IllegalArgumentException "String must not be empty" — typically during the
hot-attribute query-index rebuild in select().

Replace the try\! sites in appendPending()/ensureMaterialized() and the
single-attribute fallback with a throwing helper invoked via try?, dropping any
attribute that fails validation. This matches jsoup, which silently drops
malformed empty-key attributes, and makes materialization resilient to any
validation failure rather than only the empty-key case.

Adds a public-API regression test (boolean, valued, and meta selector cases).

Fixes #392
@aehlke

aehlke commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

LGTM. Thanks so much!!! Very glad to have this solved properly

@aehlke
aehlke merged commit 2b0d5b0 into scinfu:master Jun 19, 2026
3 checks passed
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.

Fatal crash on whitespace-only attribute key (Attributes.swift try! sites)

2 participants