Skip to content

Commit 22edf3f

Browse files
committed
Fix: make sure to allocate a XML::Node once per LibXML::Node*
1 parent 7fd366f commit 22edf3f

File tree

1 file changed

+63
-11
lines changed

1 file changed

+63
-11
lines changed

src/xml/node.cr

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,75 @@ class XML::Node
22
LOOKS_LIKE_XPATH = /^(\.\/|\/|\.\.|\.$)/
33

44
# Every Node must keep a reference to its document Node. To keep things
5-
# simple, a document Node merely references itself.
5+
# simple, a document Node merely references itself. An unlinked node must
6+
# still reference its original document Node until adopted into another
7+
# document's tree.
68
@document : Node
79

8-
# Unlinked Nodes must still reference its original document Node. They don't
9-
# appear in the document's tree anymore, and they won't be freed along with
10-
# the document, but we still need to access some data on the document's Node,
11-
# and thus need to keep it alive.
10+
# Remembers subtree nodes. Avoids allocating a XML::Node twice for the same
11+
# libxml node. This allows to keep centralized information about the nodes so
12+
# the finalizer won't try to access the libxml node to detect some state
13+
# —which is unsafe because it may have been freed already, and won't try to
14+
# free an unlinked node twice (or leak unlinked libxml nodes).
15+
protected getter! cache : Hash(UInt64, UInt64)?
16+
17+
# Unlinked Nodes must still reference their original document Node. They don't
18+
# appear in the document's tree anymore and thus won't be freed along with the
19+
# document, but the libxml node still referes to the libxml doc and thus need
20+
# to keep the document alive.
1221
@unlinked = false
1322

1423
# :nodoc:
15-
def initialize(node : LibXML::Doc*, @errors : Array(XML::Error)? = nil)
16-
@node = node.as(LibXML::Node*)
24+
#
25+
# Allocates a XML::Node for a libxml document node once, so we don't finalize
26+
# a document twice. We can store the pointer right into the libxml struct
27+
# because the XML::Node lives as long as the libxml doc.
28+
def self.new(doc : LibXML::Doc*, errors : Array(Error)? = nil)
29+
if ptr = doc.value._private
30+
ptr.as(Node)
31+
else
32+
new(doc_: doc, errors_: errors)
33+
end
34+
end
35+
36+
# :nodoc:
37+
def self.new(node : LibXML::Node*, document : self) : self
38+
# should never happen, but just in case
39+
return document if node == document.@node
40+
41+
cache = document.cache
42+
if (addr = cache[node.address]?) && addr != 0
43+
return Pointer(Void).new(addr).as(Node)
44+
end
45+
46+
this = new(node_: node, document_: document)
47+
cache[node.address] = this.as(Void*).address
48+
this
49+
end
50+
51+
# :nodoc:
52+
@[Deprecated("Use XML::Node.new(node, document) instead.")]
53+
def self.new(node : LibXML::Node*) : self
54+
new(node, new(node.value.doc))
55+
end
56+
57+
private def initialize(*, doc_ : LibXML::Doc*, errors_ : Array(Error)?)
58+
@node = doc_.as(LibXML::Node*)
59+
@errors = errors_
60+
@cache = Hash(UInt64, UInt64).new
1761
@document = uninitialized Node
1862
@document = self
63+
doc_.value._private = self.as(Void*)
1964
end
2065

21-
# :nodoc:
22-
def initialize(@node : LibXML::Node*, @document : Node)
66+
private def initialize(*, node_ : LibXML::Node*, document_ : self)
67+
@node = node_.as(LibXML::Node*)
68+
@document = document_
2369
end
2470

2571
# :nodoc:
2672
def finalize
27-
if document?
73+
if @document == self
2874
# free the document, which will recursively free the DOM tree, NS, ...
2975
LibXML.xmlFreeDoc(@node.as(LibXML::Doc*))
3076
elsif @unlinked
@@ -575,8 +621,14 @@ class XML::Node
575621

576622
# Removes the node from the XML document.
577623
def unlink : Nil
578-
LibXML.xmlUnlinkNode(self)
624+
return if @unlinked
625+
579626
@unlinked = true
627+
LibXML.xmlUnlinkNode(self)
628+
end
629+
630+
def unlinked? : Bool
631+
@unlinked
580632
end
581633

582634
# Returns `true` if this is an xml Document node.

0 commit comments

Comments
 (0)