-
-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathnode.go
More file actions
1808 lines (1664 loc) · 68.7 KB
/
Copy pathnode.go
File metadata and controls
1808 lines (1664 loc) · 68.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package helium
import (
"bytes"
"errors"
"fmt"
"slices"
"github.com/lestrrat-go/helium/internal/nodelink"
)
// AsNode performs a safe type assertion on a [Node], returning the
// concrete type T and true if the assertion succeeds, or the zero value
// of T and false otherwise.
//
// if elem, ok := helium.AsNode[*helium.Element](node); ok {
// // use elem
// }
//
// A typed-nil pointer stored in a non-nil Node interface (Go's interface nil
// trap — e.g. the *Element returned by [Document.DocumentElement] for a
// document with no root) reports (zero, false), never (nil, true): a caller
// that gets ok == true can always safely dereference the result.
func AsNode[T Node](n Node) (T, bool) {
var zero T
if n == nil {
return zero, false
}
v, ok := n.(T)
if !ok {
return zero, false
}
// The assertion matched, so v is about to be returned as ok. Reject a
// typed-nil pointer here (isNilNode only when the assertion already
// succeeded, so ordinary calls skip the reflect check) so callers never
// receive a non-nil (T, true) wrapping a nil pointer.
if isNilNode(v) {
return zero, false
}
return v, true
}
// Node is a read-only view of an XML document tree node (libxml2: xmlNode).
type Node interface {
baseDocNode() *docnode // prevents external implementation
Content() []byte
FirstChild() Node
LastChild() Node
Line() int
Name() string
NextSibling() Node
OwnerDocument() *Document
Parent() Node
PrevSibling() Node
Type() ElementType
}
// MutableNode extends Node with tree-mutation operations.
type MutableNode interface {
Node
AddChild(Node) error
AddSibling(Node) error
// AppendText appends text content to this node (libxml2: xmlNodeAddContent).
AppendText([]byte) error
Replace(...Node) error
SetLine(int)
SetOwnerDocument(doc *Document)
SetTreeDoc(doc *Document)
}
// Raw single-pointer linkage (parent/prev/next) is deliberately NOT part of
// MutableNode. Those pointers are maintained by the guarded AddChild /
// AddSibling / Replace / UnlinkNode operations, which keep the reciprocal
// back-pointers consistent and reject cycles. The unchecked primitives that set
// exactly one pointer live behind the explicitly-unsafe UnsafeSet* package
// functions below, so ordinary tree mutation cannot reach them by accident.
// docnode is responsible for handling the basic tree-ish operations
type docnode struct {
name string
etype ElementType
firstChild Node
lastChild Node
parent Node
next Node
prev Node
doc *Document
line int
entityBaseURI string // non-empty when this node originates from an external parsed entity
}
// node represents a node in a XML tree.
type node struct {
docnode
// private interface{}
content []byte
// properties is the head of the element's attribute chain, linked through
// each Attribute's next pointer. It is built exclusively through the guarded
// property-splice (Element.addProperty) and Attribute.AddSibling paths, which
// reject self/cycle insertion and never install a foreign link, so a
// well-formed chain is a short, self-owned, acyclic list. The hot
// attribute-lookup walks (Element.addProperty / HasAttribute / Attributes /
// ForEachAttribute) therefore traverse it with a plain NextAttribute loop and
// no per-list cycle guard. Whole-tree walks that may be handed an
// externally-corrupted chain (setTreeDoc, the serializer) do carry a cheap
// per-list seen guard.
properties *Attribute
ns *Namespace
nsDefs []*Namespace
qname string // cached qualified name (prefix:local or just local)
}
// ElementType identifies the kind of a DOM node (element, text, comment, and so
// on). It mirrors libxml2's xmlElementType. Use it to distinguish node kinds
// returned by the Node interface's Type method; the enumerated constants below
// name each kind.
type ElementType int
const (
ElementNode ElementType = iota + 1
AttributeNode
TextNode
CDATASectionNode
EntityRefNode
EntityNode
ProcessingInstructionNode
CommentNode
DocumentNode
DocumentTypeNode
DocumentFragNode
NotationNode
HTMLDocumentNode
DTDNode
ElementDeclNode
AttributeDeclNode
EntityDeclNode
NamespaceDeclNode
XIncludeStartNode
XIncludeEndNode
// NamespaceNode represents a namespace declaration (does not exist in libxml2).
NamespaceNode
)
const _ElementType_name = "ElementNodeAttributeNodeTextNodeCDATASectionNodeEntityRefNodeEntityNodeProcessingInstructionNodeCommentNodeDocumentNodeDocumentTypeNodeDocumentFragNodeNotationNodeHTMLDocumentNodeDTDNodeElementDeclNodeAttributeDeclNodeEntityDeclNodeNamespaceDeclNodeXIncludeStartNodeXIncludeEndNodeNamespaceNode"
var _ElementType_index = [...]uint16{0, 11, 24, 32, 48, 61, 71, 96, 107, 119, 135, 151, 163, 179, 186, 201, 218, 232, 249, 266, 281, 294}
func (i ElementType) String() string {
i -= 1
if i < 0 || i >= ElementType(len(_ElementType_index)-1) {
return fmt.Sprintf("ElementType(%d)", i+1)
}
return _ElementType_name[_ElementType_index[i]:_ElementType_index[i+1]]
}
// NamespaceContainer is an interface for nodes that carry namespace declarations.
type NamespaceContainer interface {
Namespaces() []*Namespace
}
// Namespacer is an interface for things that have a namespace
// prefix and URI.
type Namespacer interface {
Namespace() *Namespace
Namespaces() []*Namespace
Prefix() string
URI() string
LocalName() string
}
// because docnode contains links to other nodes, one tends to want to make
// methods for docnodes that cover the rest of the Node types. However,
// this cannot be done because the way Go does method reuse -- by delegation.
// For example, a method that changes the parent's point to the current node would
// be bad:
//
// func (n *docnode) MakeMeYourParent(cur Node) {
// cur.baseDocNode().parent = n
// }
//
// Wait, you just passed a pointer to the docnode, not the container node
// such as Element, Text, Comment, etc.
//
// So basically the deal is: if you need methods that may mutate the current
// node AND the operand node, DO NOT implement it for docnode. That includes
// things like AddSibling, or AddChild.
func (n *docnode) baseDocNode() *docnode {
return n
}
func setFirstChild(n MutableNode, cur Node) {
n.baseDocNode().firstChild = cur
}
func setLastChild(n MutableNode, cur Node) {
n.baseDocNode().lastChild = cur
}
// SetOwnerDocument makes doc this node's owning document.
func (n *docnode) SetOwnerDocument(doc *Document) {
n.doc = doc
}
func (n docnode) OwnerDocument() *Document {
return n.doc
}
func (n docnode) Parent() Node {
return n.parent
}
// Content aggregates the content of this node's own children. It advances
// between children with the owned-boundary rule (nextOwnedChild): a foreign
// child — an entity reference's shared Entity child, owned by the DTD, whose
// sibling pointers belong to the DTD declaration list — ends the aggregation
// instead of spilling into another list's siblings, and a per-list seen set
// stops a cyclic sibling pointer from looping forever. The receiver is a pointer
// so it is the real owning node against which child ownership is checked. The
// recursion into a container child's own subtree carries an ACTIVE-PATH set, so
// a pure child-pointer cycle (element -> element -> ... -> element, not routed
// through an Entity's terminating stored-text Content) terminates on the
// back-edge instead of recursing forever.
func (n *docnode) Content() []byte {
b := bytes.Buffer{}
aggregateOwnedContent(n, &b, map[*docnode]struct{}{n: {}})
return b.Bytes()
}
// aggregateOwnedContent appends the concatenated content of n's own children to
// b. onPath is the set of container docnodes currently being aggregated (n
// inclusive): a child already on that path is a back-edge (a child-pointer
// cycle) and is skipped so the recursion terminates. onPath is an ACTIVE-PATH
// set, not a global visited set, so a shared DAG node reached on a different
// path is still re-aggregated per occurrence. A per-list seen set independently
// bounds a cyclic sibling pointer within one child list.
func aggregateOwnedContent(n *docnode, b *bytes.Buffer, onPath map[*docnode]struct{}) {
seen := make(map[*docnode]struct{})
for child := n.firstChild; child != nil; child = nextOwnedChild(n, child) {
cdn := child.baseDocNode()
if _, dup := seen[cdn]; dup {
break
}
seen[cdn] = struct{}{}
if _, active := onPath[cdn]; active {
continue
}
// A leaf child (Text/Comment/CDATA/PI/Entity/NS wrapper) overrides
// Content() with self-contained text that cannot loop, so call it
// directly. Any other node aggregates its own children through this same
// docnode path, so recurse under the active-path guard.
if aggregatesOwnContent(child) {
onPath[cdn] = struct{}{}
aggregateOwnedContent(cdn, b, onPath)
delete(onPath, cdn)
continue
}
_, _ = b.Write(child.Content())
}
}
// aggregatesOwnContent reports whether n's Content() is the child-aggregating
// docnode implementation (a container), as opposed to a self-contained leaf
// override. The leaf types enumerated here store their text directly and their
// Content() cannot recurse; every other node type — including any future
// container — aggregates its children and must be recursed under the
// active-path cycle guard.
func aggregatesOwnContent(n Node) bool {
switch n.(type) {
case *Text, *Comment, *CDATASection, *ProcessingInstruction, *Entity, *NamespaceNodeWrapper:
return false
default:
return true
}
}
// rawContentNode is implemented by leaf nodes (Text, Comment, CDATASection)
// that store their textual content in an internal mutable byte slice. It
// exposes that slice directly (without the defensive copy the exported
// Content() makes) for internal read-only hot paths such as serialization.
type rawContentNode interface {
rawContent() []byte
}
// rawContent returns the internal content byte slice of n WITHOUT copying when
// n is a leaf node that aliases its content (Text, Comment, CDATASection).
// Callers MUST treat the result as read-only; mutating it corrupts the DOM.
// For any other node it falls back to the (already copy-safe) Content().
func rawContent(n Node) []byte {
if rc, ok := n.(rawContentNode); ok {
return rc.rawContent()
}
return n.Content()
}
func appendText(n MutableNode, b []byte) error {
// Fast path: if last child is already a text node, append directly
// without allocating a new Text node.
if last := n.LastChild(); last != nil {
if t, ok := AsNode[*Text](last); ok {
return t.AppendText(b)
}
}
// Use slab allocator when the node belongs to a document.
if doc := n.OwnerDocument(); doc != nil {
t := doc.CreateText(b)
return n.AddChild(t)
}
t := newText(b)
return n.AddChild(t)
}
// NodeWalker visits nodes during tree traversal.
type NodeWalker interface {
Visit(Node) error
}
// NodeWalkerFunc is an adapter to allow use of ordinary functions as NodeWalker.
// Similar to http.HandlerFunc.
type NodeWalkerFunc func(Node) error
func (f NodeWalkerFunc) Visit(n Node) error {
return f(n)
}
// Walk performs a depth-first traversal of the node tree rooted at n,
// calling w.Visit for each node. There is no direct libxml2 equivalent; callers
// typically write manual tree traversal loops in C.
//
// Walk is safe on hand-built or foreign-linked graphs that a plain
// child-pointer descent would loop on. It advances between siblings using the
// OWNED-BOUNDARY rule — a child whose Parent() is not the frame's node (an
// entity reference's shared Entity child, owned by the DTD, whose sibling
// pointers belong to another list) ends that child list — so the traversal
// never wanders out of a node's own children. It also carries the set of nodes
// currently on the DFS stack (the active path): descending into a node already
// on that path is a back-edge (a cycle), and Walk returns ErrWalkCycle instead
// of looping. Memory is O(active-path depth). A shared DAG node reached on a
// different path (not currently on the stack) is not a cycle and is still
// visited on each occurrence — Walk does not maintain a global visited set, so
// DAG traversal is unchanged. On an acyclic, parent-consistent tree behavior is
// identical to a naive recursive descent.
func Walk(n Node, w NodeWalker) error {
// Reject both a literal nil interface and a typed-nil pointer (e.g. the
// *Element that Document.DocumentElement returns for a rootless document)
// with the matchable ErrNilNode, before any baseDocNode() dereference that
// would panic on a typed nil.
if isNilNode(n) {
return ErrNilNode
}
type walkFrame struct {
node Node
entered bool
activeChild Node
// seenChildren records every child of node this frame has already
// enumerated, so a child that repeats within the SAME sibling list —
// a sibling cycle longer than one node (a -> b -> a, all siblings of
// node) — is detected. The active-path guard alone misses it: each
// child is popped and removed from onPath before its next sibling is
// examined, so the enumeration would otherwise spin forever.
seenChildren map[*docnode]struct{}
}
onPath := make(map[*docnode]struct{})
stack := []walkFrame{{node: n}}
for len(stack) > 0 {
top := &stack[len(stack)-1]
if !top.entered {
if err := w.Visit(top.node); err != nil {
return err
}
top.entered = true
onPath[top.node.baseDocNode()] = struct{}{}
top.activeChild = top.node.FirstChild()
continue
}
if top.activeChild == nil {
delete(onPath, top.node.baseDocNode())
stack = stack[:len(stack)-1]
if len(stack) > 0 {
parent := &stack[len(stack)-1]
parent.activeChild = nextWalkSibling(parent.node, parent.activeChild)
}
continue
}
childKey := top.activeChild.baseDocNode()
if _, cyclic := onPath[childKey]; cyclic {
return ErrWalkCycle
}
if _, dup := top.seenChildren[childKey]; dup {
return ErrWalkCycle
}
if top.seenChildren == nil {
top.seenChildren = make(map[*docnode]struct{})
}
top.seenChildren[childKey] = struct{}{}
// top may dangle after the append reallocates stack; mark before it.
stack = append(stack, walkFrame{node: top.activeChild})
}
return nil
}
// nextWalkSibling advances child to the next sibling within owner's own child
// list, applying the owned-boundary rule. It does NOT special-case a
// self-referential sibling pointer (child.next == child): the duplicate flows
// back to the caller so the per-frame seenChildren set detects it and Walk
// returns ErrWalkCycle, exactly as it does for a longer sibling cycle
// (a -> b -> a). Silently terminating the self-loop here would instead let Walk
// report SUCCESS on a corrupt one-node sibling cycle.
func nextWalkSibling(owner Node, child Node) Node {
return nextOwnedChild(owner.baseDocNode(), child)
}
func (n docnode) LocalName() string {
return n.name
}
func (n docnode) Name() string {
return n.name
}
func (n docnode) Type() ElementType {
return n.etype
}
func (n docnode) Line() int {
return n.line
}
func (n *docnode) SetLine(line int) {
n.line = line
}
func (n docnode) FirstChild() Node {
return n.firstChild
}
func (n docnode) LastChild() Node {
return n.lastChild
}
// wouldCreateCycle reports whether installing cur under parent would create a
// cycle. That happens when parent is cur itself or is already reachable from
// cur: closing the link parent->cur then forms the loop parent -> cur -> ... ->
// parent.
//
// Walking parent's ANCESTOR chain (inclusive of parent) and looking for cur
// covers every such case — including the self-insertion cur == parent — at
// O(depth(parent)) WHEN parent/child links are consistent. But a child link may
// point at a node whose own parent pointer points elsewhere: an entity
// reference's child is the shared Entity node, whose parent stays the DTD
// (mirroring libxml2). A cycle formed through such a foreign link (e.g.
// ent.AddChild(ref) where ref's child is ent) is invisible to the ancestor
// walk, so when cur has children we additionally verify parent is not reachable
// from cur by following CHILD pointers. The parser hot path appends childless
// leaves and skips that second descent entirely.
func wouldCreateCycle(parent, cur Node) bool {
cdn := cur.baseDocNode()
for anc := parent; anc != nil; anc = anc.Parent() {
if anc.baseDocNode() == cdn {
return true
}
}
if parent == nil || cdn.firstChild == nil {
return false
}
return childReaches(cur, parent.baseDocNode())
}
// childReachesInlineCap is the number of popped nodes childReachesVisited
// tracks in its inline array before promoting to a map. It selects a DATA
// STRUCTURE only, never a search cutoff: above the cap the search continues
// exactly as before, backed by a map instead of a linear scan. Instrumentation
// across the xslt3 conformance suite recorded 2,119,653 childReaches calls
// popping 6,457,455 nodes total (mean 3.05, max 10,922), with 94.1% of calls
// popping 2 or fewer nodes, so 64 clears nearly every real call while still
// bounding the linear scan's cost on the rare deep one.
const childReachesInlineCap = 64
// childReachesVisited is the popped-node visited set for childReaches. It
// starts as a fixed-size array scanned linearly — no allocation — and
// promotes itself to a map once more than childReachesInlineCap distinct
// nodes have been recorded, from which point the map is authoritative. This
// mirrors the measured call distribution: almost every call stays entirely in
// the array, and only the rare call with a very large subtree pays for a map.
type childReachesVisited struct {
inline [childReachesInlineCap]*docnode
n int
m map[*docnode]struct{}
}
// has reports whether dn was previously recorded by add.
func (v *childReachesVisited) has(dn *docnode) bool {
if v.m != nil {
_, ok := v.m[dn]
return ok
}
for i := range v.n {
if v.inline[i] == dn {
return true
}
}
return false
}
// add records dn as visited, promoting the inline array to a map the first
// time the array fills up.
func (v *childReachesVisited) add(dn *docnode) {
if v.m != nil {
v.m[dn] = struct{}{}
return
}
if v.n < len(v.inline) {
v.inline[v.n] = dn
v.n++
return
}
v.m = make(map[*docnode]struct{}, v.n+1)
for i := range v.inline {
v.m[v.inline[i]] = struct{}{}
}
v.m[dn] = struct{}{}
}
// childReaches reports whether target is reachable from node by following child
// pointers (node inclusive). It walks ITERATIVELY with an explicit stack and a
// visited set, so it terminates on any child graph — shared (DAG) or hand-built
// cyclic — visiting each node at most once and never overflowing the goroutine
// stack on a deep tree. It is SOUND: it never bails out early, so a cycle at ANY
// depth is detected (a depth cap here would fail OPEN and admit a deep cycle).
// It enumerates each node's OWN children via nextOwnedSibling so a foreign child
// link (an entity reference's Entity child, owned by the DTD) is not followed
// into another list's siblings.
//
// The inner sibling enumeration is bounded by [siblingCycleGuard] — the same
// allocation-free Brent's-algorithm guard [Children]/[ChildElements]/
// [Descendants] already use — rather than a per-call sibling-seen set. The
// trade is where the enumeration stops: a seen set stops at the exact first
// repeat, while siblingCycleGuard stops within a small multiple of the cycle
// length, so on a corrupt sibling list a few nodes may be pushed onto the stack
// more than once. That is harmless here — the popped-node visited set
// deduplicates those extra pushes on pop, and the overshoot is bounded — and
// termination is unconditional either way.
func childReaches(node Node, target *docnode) bool {
var visited childReachesVisited
stack := []Node{node}
for len(stack) > 0 {
dn := stack[len(stack)-1].baseDocNode()
stack = stack[:len(stack)-1]
if dn == target {
return true
}
if visited.has(dn) {
continue
}
visited.add(dn)
var g siblingCycleGuard
for child := dn.firstChild; child != nil; {
cdn := child.baseDocNode()
if g.step(cdn) {
break
}
stack = append(stack, child)
child = nextOwnedSibling(dn, cdn)
}
}
return false
}
// nextOwnedChild returns the next sibling of child within owner's child list, or
// nil when child is foreign-owned (its parent is not owner). A foreign child's
// sibling pointers belong to another list — an entity reference's Entity child
// is owned by the DTD — so following them would walk out of owner's children.
func nextOwnedChild(owner *docnode, child Node) Node {
cp := child.Parent()
if cp == nil || cp.baseDocNode() != owner {
return nil
}
return child.NextSibling()
}
// nextOwnedSibling returns the next sibling of cdn within owner's child list, or
// nil when cdn is foreign-owned. It is nextOwnedChild for a caller that already
// holds the child's *docnode, and applies the identical owned-boundary rule: a
// foreign child's sibling pointers belong to another list — an entity
// reference's Entity child is owned by the DTD — so following them would walk
// out of owner's children.
//
// It reads cdn.parent and cdn.next as FIELDS where nextOwnedChild calls Parent()
// and NextSibling(). Those methods have VALUE receivers on docnode, which is 136
// bytes, so every interface call copies the struct, and the iterators in iter.go
// pay that on each child. The field read yields the same value without the copy.
//
// This is a SPEED choice and carries no semantics. It holds only while no node
// type overrides FirstChild/Parent/NextSibling — none does, and
// TestNodeLinkAccessorsMatchFields pins it. Should one ever need to, this helper
// and the owner.firstChild reads at the head of each iterator loop return to the
// method calls with no other change and no difference in behavior.
func nextOwnedSibling(owner, cdn *docnode) Node {
p := cdn.parent
if p == nil || p.baseDocNode() != owner {
return nil
}
return cdn.next
}
// destinationDocument returns the document a node inserted under n would belong
// to. For a Document receiver that is the document itself; for any other node it
// is the node's owning document.
func destinationDocument(n MutableNode) *Document {
if d, ok := n.(*Document); ok {
return d
}
return n.OwnerDocument()
}
// noteCrossDocumentEscape records that cur is being linked into a different
// document than the one that owns it. A node's backing storage (its struct and
// any text-content bytes) is drawn from its owning document's slab allocator, so
// once the node is referenced from another document that owning document must no
// longer recycle its slab chunks on Free — a later parse could otherwise reuse a
// chunk still holding the moved node and overwrite it. Marking the SOURCE
// document turns its Free into a no-op (GC reclaims the still-referenced chunks
// instead). A nil owner (a heap-allocated standalone node) has no slab to guard.
func noteCrossDocumentEscape(dest *Document, cur Node) {
curDoc := cur.OwnerDocument()
if curDoc == dest {
return
}
if curDoc == nil {
return
}
curDoc.slabEscaped = true
}
// noteCrossDocumentNamespaceEscape is the Namespace counterpart of
// noteCrossDocumentEscape. A slab-backed Namespace is drawn from its owning
// document's namespace slab (its owner is Namespace.context, set by
// Document.CreateNamespace; a heap-allocated Namespace has a nil context and no
// slab). When AddNamespaceDecl retains such a Namespace in another document's
// declarations, or SetNamespace installs it as another document's node's active
// namespace, the owning document must no longer recycle its namespace slab on
// Free, so mark it escaped exactly as a cross-document node move does.
func noteCrossDocumentNamespaceEscape(dest *Document, ns *Namespace) {
if ns == nil {
return
}
src := ns.context
if src == nil || src == dest {
return
}
src.slabEscaped = true
}
// addChildPreflight runs the shared self/cycle guard and auto-unlink that every
// AddChild path must perform before relinking. It returns a non-nil error when
// the operation must be rejected; on success cur is detached from any previous
// position and safe to splice in. Leaf AddChild overrides (Text, Comment, ...)
// reuse this so their content-merge fast paths cannot bypass the guard: a node
// must not be merged into itself, and an already-linked incoming node must be
// unlinked from its old parent first.
func addChildPreflight(n MutableNode, cur Node) error {
cdn := cur.baseDocNode()
// A node linked into a different document keeps its slab-backed storage in its
// original document, so guard that document's Free against recycling it. Mark
// BEFORE any unlink, while cur still reports its original owner.
noteCrossDocumentEscape(destinationDocument(n), cur)
// Cycle guard: a node may not be inserted into itself, nor into one of
// its own descendants (which would make an ancestor a descendant of
// itself). This also catches the self-insertion case when n == cur.
if wouldCreateCycle(n, cur) {
return fmt.Errorf("%w: cannot add a node as a child of itself or one of its descendants", ErrCyclicNode)
}
// Detach cur from its current parent/sibling chain before relinking, so a
// node that already lives elsewhere in a tree cannot remain in two places.
// unlinkNode works for every sealed node type, including non-MutableNode
// nodes such as NamespaceNodeWrapper, so the detach can never be silently
// skipped and leave stale old-parent links behind.
if cdn.parent != nil || cdn.prev != nil || cdn.next != nil {
unlinkNode(cur)
}
return nil
}
func addChild(n MutableNode, cur Node) error {
// Reject a nil or typed-nil operand BEFORE any baseDocNode() dereference so
// the call returns ErrNilNode instead of panicking and leaves the tree
// untouched.
if isNilNode(cur) {
return ErrNilNode
}
// An Attribute is never an ordinary child node. On an *Element it belongs in
// the properties list, mirroring libxml2's xmlAddChild, which routes an
// attribute operand into the parent's properties (replacing a same-named one)
// and never into the child list; on any other parent an attribute has no valid
// placement and is rejected. Handle this BEFORE the generic child-splice (and
// before any leaf content-merge fast path, which the leaf AddChild overrides
// reach only for text-like operands) so an attribute can never land in a child
// list and serialize as a spurious child element.
if attr, ok := cur.(*Attribute); ok {
elem, ok := n.(*Element)
if !ok {
return fmt.Errorf("%w: cannot add an attribute as a child of a %s node; attributes belong on an element", ErrInvalidOperation, n.Type())
}
// Preflight through the normal guards (cross-document escape marking,
// auto-unlink from any previous parent/property chain). An attribute cannot
// form a child-list cycle, but it must be detached from a prior location
// before addProperty splices it in.
if err := addChildPreflight(elem, attr); err != nil {
return err
}
elem.addProperty(attr)
return nil
}
pdn := n.baseDocNode()
cdn := cur.baseDocNode()
// CreateReference stores the DTD-owned Entity as both child endpoints while
// leaving the Entity's parent and sibling links on the DTD declaration list.
// Re-adding that same stored foreign endpoint is already represented on n, so
// treat it as a no-op. Auto-unlinking it would remove the declaration from its
// real owner before resolveOwnedTail could recognize the foreign child list.
foreignParent := cdn.parent != nil && cdn.parent.baseDocNode() != pdn
isFirstChild := pdn.firstChild != nil && pdn.firstChild.baseDocNode() == cdn
isLastChild := pdn.lastChild != nil && pdn.lastChild.baseDocNode() == cdn
if foreignParent && (isFirstChild || isLastChild) {
return nil
}
if err := addChildPreflight(n, cur); err != nil {
return err
}
// A nil tail means pdn has no child it owns. Install cur as the owned child
// list instead of linking through a foreign head whose sibling links belong
// to another parent.
l := resolveOwnedTail(n, pdn)
if l == nil {
pdn.firstChild = cur
pdn.lastChild = cur
cdn.parent = n
return nil
}
ldn := l.baseDocNode()
curType := cdn.etype
// Fast path: when lastChild has no next sibling (the normal case),
// link directly without virtual dispatch through AddSibling.
if ldn.next == nil && (curType != TextNode || ldn.etype != TextNode) {
ldn.next = cur
cdn.prev = l
cdn.parent = n
pdn.lastChild = cur
return nil
}
// AddSibling handles setting the parent, and the
// lastChild pointer (also merges adjacent text nodes)
if err := l.(MutableNode).AddSibling(cur); err != nil { //nolint:forcetypeassert
return err
}
// If the last child was a text node, keep the old LastChild
if curType == TextNode && ldn.etype == TextNode {
pdn.lastChild = l
}
return nil
}
func (n docnode) NextSibling() Node {
if n.next == nil {
return nil
}
return n.next
}
func (n docnode) PrevSibling() Node {
return n.prev
}
// addSiblingPreflight runs the shared self/cycle guard and auto-unlink that
// every AddSibling path must perform before relinking. It returns a non-nil
// error when the operation must be rejected; on success cur is detached from
// any previous position and safe to splice in. Text.AddSibling reuses this so
// its text-merge fast path cannot bypass the guard.
func addSiblingPreflight(n MutableNode, cur Node) error {
cdn := cur.baseDocNode()
// A sibling of n shares n's document; if cur comes from elsewhere, guard its
// original document's Free against recycling its slab storage. Mark BEFORE any
// unlink, while cur still reports its original owner. See noteCrossDocumentEscape.
noteCrossDocumentEscape(n.OwnerDocument(), cur)
// Cycle guard: a sibling of n is installed under n's parent, so the same
// self/ancestor rule that protects addChild applies here against the
// effective insertion parent. This also rejects cur == n (a node cannot be
// its own sibling) since n is its parent's child.
if cur.baseDocNode() == n.baseDocNode() || wouldCreateCycle(n.Parent(), cur) {
return fmt.Errorf("%w: cannot add a node as a sibling of itself or one of its descendants", ErrCyclicNode)
}
// Detach cur from its current parent/sibling chain before relinking, so a
// node that already lives elsewhere in a tree cannot remain in two places.
// unlinkNode works for every sealed node type, including non-MutableNode
// nodes such as NamespaceNodeWrapper, so the detach can never be silently
// skipped and leave stale old-parent links behind.
if cdn.parent != nil || cdn.prev != nil || cdn.next != nil {
unlinkNode(cur)
}
return nil
}
// resolveOwnedTail returns the node an append onto pdn must link after, or nil
// when pdn has no child it owns. It never returns a node pdn does not own from
// a chain pdn does not reach, which separates it from a bare pdn.lastChild read.
//
// The recorded tail and the reachable child list can disagree, and safe API
// builds every direction of that disagreement. A copied external subset claims
// the document as its parent while living only in extSubset, so appending
// through it records a tail that is on no child list and leaves firstChild nil;
// stringToNodeList materializes an entity's replacement children with
// firstChild set and lastChild nil; CreateReference installs the DTD's shared
// Entity as the reference's firstChild while that Entity goes on claiming the
// DTD. Trusting lastChild alone loses the reachable list in the first shape,
// discards it in the second, and crosses into the DTD chain in the third. The
// record is used only when it proves itself, and the owned list is walked
// otherwise.
//
// The walk stops at the first node that does not claim pdn, because the chain
// beyond such a node belongs to whichever parent it does claim and an append
// must not run off into it. When the first node is foreign, there is no owned
// append anchor. Returning that foreign head would let AddSibling follow and
// mutate its owner's sibling chain, so the caller replaces the non-owned child
// pointer with a new owned list. addChild handles the one different case before
// calling this resolver: re-adding that stored foreign endpoint is a no-op, so
// its real owner's chain and the foreign child pointer both stay unchanged.
//
// Healthy trees take the O(1) route: two pointer comparisons on top of the read
// the callers already did. Only a tree already carrying a stale record pays the
// walk. An owned tail matches the node addSibling would reach; a foreign head
// returns nil so the caller never delegates into another parent's chain.
func resolveOwnedTail(parent Node, pdn *docnode) Node {
// No child list means no tail to link after, whatever lastChild records.
// This is the copied-external-subset shape: the subset claims the document
// as its parent while living only in extSubset, so appending through it
// records a tail while firstChild stays nil.
first := pdn.firstChild
if first == nil {
return nil
}
// Trust the recorded tail only when it proves itself AND this parent has not
// been handed a child that claims it from off its child list. Without that
// second condition a node can claim this parent from another chain entirely,
// and linking behind it would abandon the reachable list. tailJumpTarget
// declines on the same signal, so both append routes degrade together.
if l := pdn.lastChild; l != nil {
ldn := l.baseDocNode()
if ldn.next == nil && ldn.parent != nil && ldn.parent.baseDocNode() == pdn && !holdsOffChainChildClaim(parent) {
return l
}
}
// The record is unusable, so walk to the last node that still claims pdn,
// bounded by the same allocation-free guard the iterators use. If the first
// node is foreign, the walk returns nil instead of delegating AddSibling into
// that node's owning chain.
var g siblingCycleGuard
var tail Node
for cur := first; cur != nil; {
cdn := cur.baseDocNode()
if g.step(cdn) {
break
}
if cdn.parent == nil || cdn.parent.baseDocNode() != pdn {
break
}
tail = cur
cur = cdn.next
}
return tail
}
// holdsOffChainChildClaim reports whether THIS parent has been handed a child
// that claims it while sitting on no child list of its own, which is the one
// signal that a self-proving lastChild may still belong to another chain. The
// record is per-PARENT: a claim on one parent says nothing about any other
// parent, including every other parent in the same document, so it must never
// be read from the owning document.
//
// The only claimant safe API creates is an external subset copied through
// CopyExtSubset or CopyDTDSubsets. It is given the destination document as its
// parent and left reachable only through ExtSubset. The claimed parent is that *Document
// itself, and Document.offChainChildClaim records it. (CreateInternalSubset
// also gives a DTD the document as its parent, but it splices that DTD into the
// child list, so it creates no claim.) Every other parent answers false in a
// type assertion, so an ordinary append pays nothing for the check.
func holdsOffChainChildClaim(parent Node) bool {
doc, ok := parent.(*Document)
return ok && doc.offChainChildClaim
}
// chainMember reports whether x is a member of the single child chain pdn owns:
// the chain that starts at pdn.firstChild and runs forward through next
// pointers. It answers ONE question, about the anchor of an append: may
// pdn.lastChild be taken as the tail of the chain that anchor sits on? It never
// decides whether pdn.lastChild may be WRITTEN — every write of that field is
// unconditional, exactly as it is in the sibling walk this shortcut replaces.
//
// One pointer comparison answers the anchor an append most often uses: an x that
// IS pdn.firstChild is the chain head by definition, so appending through a
// fixed early child stays O(1) per call.
//
// Every OTHER anchor costs a walk: step prev to the head of x's own chain, and
// x is a member exactly when that head is pdn.firstChild. The walk costs x's
// distance BEHIND it, never the length of the chain ahead of it, so it can
// never cost more than the NextSibling() walk it is protecting — but it does
// grow with the chain, so repeatedly appending through a fixed MIDDLE anchor
// stays quadratic and merely wins a constant factor. siblingCycleGuard bounds
// the walk, so a corrupt prev chain terminates instead of spinning.
//
// Every prev edge the proof crosses must be RECIPROCAL — the step from head to
// head.prev is taken only when that prev node points forward at head again.
// This is what a bare prev walk gets wrong. A one-way prev edge outlives any
// splice that cuts a node out of a chain from the FRONT: the node keeps pointing
// back at a neighbour that no longer points forward at it. Such a node can live
// on a chain of its OWN while still aiming at a genuine child of the parent it
// claims. Following the one-way edge would leave x's chain, arrive at
// pdn.firstChild, and "prove" a membership that does not exist — after which the
// caller would splice into the parent's real child list and abandon the rest of
// x's chain. Rejecting the non-reciprocal edge costs one pointer comparison per
// step, so the walk keeps the bound above.
func chainMember(pdn, x *docnode) bool {
if pdn == nil || x == nil {
return false
}
first := pdn.firstChild
if first == nil {
return false
}
if first.baseDocNode() == x {
return true
}
var g siblingCycleGuard
head := x
for {
if g.step(head) {
return false
}
prev := reciprocalPrev(head)
if prev == nil {
// Either head starts its chain, or its prev edge is one-way. A
// one-way edge proves nothing, so only a genuine head may match.
return head.prev == nil && first.baseDocNode() == head
}
head = prev
}
}
// reciprocalPrev returns x's previous sibling when that edge is reciprocal —
// when the prev node points forward at x again — and nil otherwise. A nil
// return therefore means "x has no usable prev edge", covering both a genuine
// chain head and a forged one-way link.
func reciprocalPrev(x *docnode) *docnode {
prev := x.prev
if prev == nil {
return nil
}
pdn := prev.baseDocNode()
if pdn.next == nil || pdn.next.baseDocNode() != x {
return nil
}
return pdn
}
// tailJumpTarget returns the node an append through anchor ndn may be spliced
// after, resolved from parent.lastChild without walking the chain ahead of the
// anchor, or nil when the append must walk instead. A nil return is never an
// error: the walk is the behavior addSibling guarantees, and this is only a
// shortcut to the node that walk would reach.
//
// The shortcut needs two facts, and only the first can be established by reading
// the neighborhood:
//
// 1. The anchor is a member of the chain parent owns. chainMember proves this,
// in a pointer comparison for an anchor that is parent.firstChild, and