Skip to content

Commit

Permalink
make Node.toString stack safe
Browse files Browse the repository at this point in the history
  • Loading branch information
lrytz committed Jul 12, 2023
1 parent ff85ece commit 63e24f5
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 44 deletions.
110 changes: 66 additions & 44 deletions shared/src/main/scala/scala/xml/Utility.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
package scala
package xml

import scala.annotation.tailrec
import scala.collection.mutable
import scala.language.implicitConversions
import scala.collection.Seq
Expand Down Expand Up @@ -187,9 +188,7 @@ object Utility extends AnyRef with parsing.TokenTests {
decodeEntities: Boolean = true,
preserveWhitespace: Boolean = false,
minimizeTags: Boolean = false): StringBuilder =
{
serialize(x, pscope, sb, stripComments, decodeEntities, preserveWhitespace, if (minimizeTags) MinimizeMode.Always else MinimizeMode.Never)
}

/**
* Serialize an XML Node to a StringBuilder.
Expand All @@ -206,35 +205,67 @@ object Utility extends AnyRef with parsing.TokenTests {
stripComments: Boolean = false,
decodeEntities: Boolean = true,
preserveWhitespace: Boolean = false,
minimizeTags: MinimizeMode.Value = MinimizeMode.Default): StringBuilder =
{
x match {
case c: Comment => if (!stripComments) c buildString sb; sb
case s: SpecialNode => s buildString sb
case g: Group =>
for (c <- g.nodes) serialize(c, g.scope, sb, stripComments, decodeEntities, preserveWhitespace, minimizeTags); sb
case el: Elem =>
// print tag with namespace declarations
sb.append('<')
el.nameToString(sb)
if (el.attributes ne null) el.attributes.buildString(sb)
el.scope.buildString(sb, pscope)
if (el.child.isEmpty &&
(minimizeTags == MinimizeMode.Always ||
(minimizeTags == MinimizeMode.Default && el.minimizeEmpty))) {
// no children, so use short form: <xyz .../>
sb.append("/>")
} else {
// children, so use long form: <xyz ...>...</xyz>
sb.append('>')
sequenceToXML(el.child, el.scope, sb, stripComments, decodeEntities, preserveWhitespace, minimizeTags)
sb.append("</")
el.nameToString(sb)
sb.append('>')
}
case _ => throw new IllegalArgumentException("Don't know how to serialize a " + x.getClass.getName)
}
minimizeTags: MinimizeMode.Value = MinimizeMode.Default
): StringBuilder = {
serializeImpl(List(x), pscope, false, stripComments, minimizeTags, sb)
sb
}

private def serializeImpl(
ns: Seq[Node],
pscope: NamespaceBinding,
spaced: Boolean,
stripComments: Boolean,
minimizeTags: MinimizeMode.Value,
sb: StringBuilder
): Unit = {
@tailrec def ser(nss: List[Seq[Node]], pscopes: List[NamespaceBinding], spaced: List[Boolean], toClose: List[Node]): Unit = nss match {
case List(ns) if ns.isEmpty =>
case ns :: rests if ns.isEmpty =>
if (toClose.head != null) {
sb.append("</")
toClose.head.nameToString(sb)
sb.append('>')
}
ser(rests, pscopes.tail, spaced.tail, toClose.tail)
case ns1 :: r =>
val (n, ns) = (ns1.head, ns1.tail)
def sp(): Unit = if (ns.nonEmpty && spaced.head) sb.append(' ')
n match {
case c: Comment =>
if (!stripComments) {
c.buildString(sb)
sp()
}
ser(ns :: r, pscopes, spaced, toClose)
case s: SpecialNode =>
s.buildString(sb)
sp()
ser(ns :: r, pscopes, spaced, toClose)
case g: Group =>
ser(g.nodes :: ns :: r, g.scope :: pscopes, false :: spaced, null :: toClose)
case e: Elem =>
sb.append('<')
e.nameToString(sb)
if (e.attributes.ne(null)) e.attributes.buildString(sb)
e.scope.buildString(sb, pscopes.head)
if (e.child.isEmpty &&
(minimizeTags == MinimizeMode.Always ||
(minimizeTags == MinimizeMode.Default && e.minimizeEmpty))) {
// no children, so use short form: <xyz .../>
sb.append("/>")
sp()
ser(ns :: r, pscopes, spaced, toClose)
} else {
sb.append('>')
val csp = e.child.forall(isAtomAndNotText)
ser(e.child :: ns :: r, e.scope :: pscopes, csp :: spaced, e :: toClose)
}
case n => throw new IllegalArgumentException("Don't know how to serialize a " + n.getClass.getName)
}
}
ser(List(ns), List(pscope), List(spaced), Nil)
}

def sequenceToXML(
children: Seq[Node],
Expand All @@ -243,20 +274,11 @@ object Utility extends AnyRef with parsing.TokenTests {
stripComments: Boolean = false,
decodeEntities: Boolean = true,
preserveWhitespace: Boolean = false,
minimizeTags: MinimizeMode.Value = MinimizeMode.Default): Unit =
{
if (children.isEmpty) return
else if (children forall isAtomAndNotText) { // add space
val it = children.iterator
val f = it.next()
serialize(f, pscope, sb, stripComments, decodeEntities, preserveWhitespace, minimizeTags)
while (it.hasNext) {
val x = it.next()
sb.append(' ')
serialize(x, pscope, sb, stripComments, decodeEntities, preserveWhitespace, minimizeTags)
}
} else children foreach { serialize(_, pscope, sb, stripComments, decodeEntities, preserveWhitespace, minimizeTags) }
}
minimizeTags: MinimizeMode.Value = MinimizeMode.Default
): Unit = if (children.nonEmpty) {
val spaced = children.forall(isAtomAndNotText)
serializeImpl(children, pscope, spaced, stripComments, minimizeTags, sb)
}

/**
* Returns prefix of qualified name if any.
Expand Down
6 changes: 6 additions & 0 deletions shared/src/test/scala/scala/xml/UtilityTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -217,4 +217,10 @@ class UtilityTest {
val x = <div>{Text(" My name ")}{Text(" is ")}{Text(" Harry ")}</div>
assertEquals(<div>My name is Harry</div>, Utility.trim(x))
}

@Test
def toStringStackSafe(): Unit = {
val xml = (1 to 5000).foldRight(<x/>) { case (_, n) => <x>{n}</x> }
xml.toString
}
}

0 comments on commit 63e24f5

Please sign in to comment.