Skip to content

Commit

Permalink
Merge pull request #1736 from lift/json-js-escaping
Browse files Browse the repository at this point in the history
Until now, we've had a hardcoded set of characters that would always be escaped
in JSON; these were never modifiable. We now plug in to the new RenderSettings
case class to add the ability to specify sets of characters that should be
escaped.

Additionally, we add a default set of such characters that corresponds to the
characters that must be escaped when JSON is evaluated directly by a JavaScript
engine rather than by a valid JSON parser (as when using JSON-P).

Lastly, we provide two default RenderSettings instances, prettyJs and
compactJs, that encapsulate the appropriate escaping with pretty or compact
rendering.
  • Loading branch information
farmdawgnation committed Dec 2, 2015
2 parents db7b181 + 1995ce4 commit 24cbbde
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 8 deletions.
72 changes: 65 additions & 7 deletions core/json/src/main/scala/net/liftweb/json/JsonAST.scala
Expand Up @@ -815,11 +815,11 @@ object JsonAST {

private[json] def quote(s: String): String = {
val buf = new StringBuilder
appendEscapedString(buf, s)
appendEscapedString(buf, s, RenderSettings.compact)
buf.toString
}

private def appendEscapedString(buf: Appendable, s: String) {
private def appendEscapedString(buf: Appendable, s: String, settings: RenderSettings) {
for (i <- 0 until s.length) {
val c = s.charAt(i)
val strReplacement = c match {
Expand All @@ -830,7 +830,8 @@ object JsonAST {
case '\n' => "\\n"
case '\r' => "\\r"
case '\t' => "\\t"
case c if ((c >= '\u0000' && c < '\u0020')) => "\\u%04x".format(c: Int)
case c if ((c >= '\u0000' && c < '\u0020')) || settings.escapeChars.contains(c) =>
"\\u%04x".format(c: Int)

case _ => ""
}
Expand All @@ -845,20 +846,69 @@ object JsonAST {
}

object RenderSettings {
/**
* Pretty-print JSON with 2-space indentation.
*/
val pretty = RenderSettings(2)
/**
* Compact print JSON on one line.
*/
val compact = RenderSettings(0)

/**
* Ranges of chars that should be escaped if this JSON is to be evaluated
* directly as JavaScript (rather than by a valid JSON parser).
*/
val jsEscapeChars =
List(('\u00ad', '\u00ad'),
('\u0600', '\u0604'),
('\u070f', '\u070f'),
('\u17b4', '\u17b5'),
('\u200c', '\u200f'),
('\u2028', '\u202f'),
('\u2060', '\u206f'),
('\ufeff', '\ufeff'),
('\ufff0', '\uffff'))
.foldLeft(Set[Char]()) {
case (set, (start, end)) =>
set ++ (start to end).toSet
}

/**
* Pretty-print JSON with 2-space indentation and escape all JS-sensitive
* characters.
*/
val prettyJs = RenderSettings(2, jsEscapeChars)
/**
* Compact print JSON on one line and escape all JS-sensitive characters.
*/
val compactJs = RenderSettings(0, jsEscapeChars)
}
/**
* RenderSettings allows for customizing how JSON is rendered to a String.
* At the moment, you can customize the indentation (if 0, all the JSON is
* printed on one line), the characters that should be escaped (in addition
* to a base set that will always be escaped for valid JSON), and whether or
* not a space should be included after a field name.
*/
case class RenderSettings(
indent: Int,
escapeChars: Set[Char] = Set(),
spaceAfterFieldName: Boolean = false
) {
val lineBreaks_? = indent > 0
}

/**
* Render `value` using `[[RenderSettings.pretty]]`.
*/
def prettyRender(value: JValue): String = {
render(value, RenderSettings.pretty)
}

/**
* Render `value` to the given `appendable` using `[[RenderSettings.pretty]]`.
*/
def prettyRender(value: JValue, appendable: Appendable): String = {
render(value, RenderSettings.pretty, appendable)
}
Expand All @@ -871,10 +921,18 @@ object JsonAST {
render(value, RenderSettings.compact)
}

/**
* Render `value` to the given `appendable` using `[[RenderSettings.compact]]`.
*/
def compactRender(value: JValue, appendable: Appendable): String = {
render(value, RenderSettings.compact, appendable)
}

/**
* Render `value` to the given `appendable` (a `StringBuilder`, by default)
* using the given `settings`. The appendable's `toString` will be called and
* the result will be returned.
*/
def render(value: JValue, settings: RenderSettings, appendable: Appendable = new StringBuilder()): String = {
bufRender(value, appendable, settings).toString()
}
Expand All @@ -895,7 +953,7 @@ object JsonAST {
case JInt(n) => buf.append(n.toString)
case JNull => buf.append("null")
case JString(null) => buf.append("null")
case JString(s) => bufQuote(s, buf)
case JString(s) => bufQuote(s, buf, settings)
case JArray(arr) => bufRenderArr(arr, buf, settings, indentLevel)
case JObject(obj) => bufRenderObj(obj, buf, settings, indentLevel)
case JNothing => sys.error("can't render 'nothing'") //TODO: this should not throw an exception
Expand Down Expand Up @@ -965,7 +1023,7 @@ object JsonAST {

(0 until currentIndent).foreach(_ => buf.append(' '))

bufQuote(name, buf)
bufQuote(name, buf, settings)
buf.append(':')
if (settings.spaceAfterFieldName) {
buf.append(' ')
Expand All @@ -986,9 +1044,9 @@ object JsonAST {
buf
}

private def bufQuote(s: String, buf: Appendable): Appendable = {
private def bufQuote(s: String, buf: Appendable, settings: RenderSettings): Appendable = {
buf.append('"') //open quote
appendEscapedString(buf, s)
appendEscapedString(buf, s, settings)
buf.append('"') //close quote
buf
}
Expand Down
40 changes: 39 additions & 1 deletion core/json/src/test/scala/net/liftweb/json/JsonAstSpec.scala
Expand Up @@ -22,7 +22,6 @@ import org.specs2.ScalaCheck
import org.scalacheck._
import org.scalacheck.Prop.{forAll, forAllNoShrink}


object JsonAstSpec extends Specification with JValueGen with ScalaCheck {
"Functor identity" in {
val identityProp = (json: JValue) => json == (json map identity)
Expand Down Expand Up @@ -142,6 +141,45 @@ object JsonAstSpec extends Specification with JValueGen with ScalaCheck {
check(forAll(anyReplacement))
}

"allow escaping arbitrary characters when serializing" in {
JsonAST.render(
JString("aaabbb"),
JsonAST.RenderSettings(0, Set('c'))
) must not be matching("a".r)
}

"escape bad JSON characters by default" in {
val allCharacters: String =
('\u0000' to '\uffff').mkString("")

val rendered =
JsonAST.render(
JString(allCharacters),
JsonAST.RenderSettings.compact
)

"[\u0000-\u0019]".r
.pattern
.matcher(rendered)
.find() must beFalse
}

"allow escaping bad JavaScript characters when serializing" in {
val allCharacters =
('\u0000' to '\uffff').mkString("")

val rendered =
JsonAST.render(
JString(allCharacters),
JsonAST.RenderSettings.compactJs
)

"[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]".r
.pattern
.matcher(rendered)
.find() must beFalse
}

"equals hashCode" in check{ x: JObject =>
val y = JObject(scala.util.Random.shuffle(x.obj))

Expand Down

0 comments on commit 24cbbde

Please sign in to comment.