Skip to content

Commit 48e8d0d

Browse files
mxaminclaude
andcommitted
Tell two same-valued attributes of one element apart (#356)
register_id's shadow body decided "the attribute being registered is already the declared one" from the *element* XPath id() returned. An element can carry the value twice: <N xml:id="dup" ID="dup"/> answers N whichever attribute is asked about, so register_id(N, 'ID') recorded a spec where the fast path registers ID, finds xml:id holding the value and raises "duplicated id.". The registry half had the same shape, by local name only: a spec for a:Id and a call for Id are two attributes, and the second cannot win the lookup either. Both halves now compare attributes. PyXmlSec_LxmlAttrValue became AttrFind, which also reports the lxml key of the attribute xmlHasProp/xmlHasNsProp would pick, and a registry spec is resolved through it on the node it was recorded for. For the declared half, a match on the element itself settles nothing when two of its attributes carry the value, and no lxml API names the declared attribute — id() names elements, and an ATTLIST without an ELEMENT leaves lxml's DTD objects empty. It is named instead by copying the document the way a whole-document shadow copies it (same base URL, same subsets) and reading that copy's own id hash. Only a value already declared for the element, on an element carrying it twice, pays for that copy. Nine collision shapes now answer identically on the raw path, the forced shadow and the mismatch build: xml:id beside ID, a DTD-declared ID beside a twin attribute (both directions), and namespaced/ unqualified registry pairs in both orders. Tests in tests/test_ds.py: the two "rejects" cases fail on the pre-fix build; the two "accepts" cases guard the no-op against an over-eager check. Validation: 318 passed / 6 skipped on the mismatch build, also at PYXMLSEC_TEST_ITERATIONS=50; 330 / 6 on the matched static wheel, plain and with PYXMLSEC_FORCE_SHADOW=1; a 10k loop of no-op plus refused registration over the DTD document with RSS 23.3 -> 24.7 MiB. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 3a50b04 commit 48e8d0d

3 files changed

Lines changed: 269 additions & 51 deletions

File tree

developer.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,22 @@ under the shadow. What lxml's own parse declared (a DTD id attribute, an
226226
`xml:id`) is read back through XPath's `id()`, the one door into lxml's id
227227
hash that passes nothing but strings and elements; what earlier
228228
`register_id`/`add_ids` calls claimed is read from the registry, a subtree
229-
spec through one XPath over its scope. Deferring the check to the replay
229+
spec through one XPath over its scope.
230+
231+
Both halves compare *attributes*, not elements: `<N xml:id="dup" ID="dup"/>`
232+
answers `N` to `id('dup')` whichever attribute is asked about, while the fast
233+
path registers `ID`, finds `xml:id` holding the value and raises. A registry
234+
spec is therefore resolved to the attribute its `xmlHasProp`/`xmlHasNsProp`
235+
would pick, and a match on the element itself only settles the declared half
236+
when a single attribute of the element carries the value — otherwise the
237+
declared attribute has to be named outright, which no lxml API does (`id()`
238+
names elements, and an `ATTLIST` without an `ELEMENT` leaves lxml's DTD
239+
objects empty). It is then named by copying the document the way a
240+
whole-document shadow copies it — same base URL, same subsets — and reading
241+
that copy's own id hash, a copy the registration is refused or recorded
242+
against anyway. Only a value already declared for the element pays for it.
243+
244+
Deferring the check to the replay
230245
instead would raise from the wrong call — a later `sign`, and then from every
231246
later call on that document — and would leave the caller believing a
232247
registration took that can never win the lookup. `add_ids` keeps its own

0 commit comments

Comments
 (0)