Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions sjsonnet/src-js/sjsonnet/Platform.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ object Platform {
}
}

// Used only for strings already proven ASCII-safe; copies low bytes without allocation.
@inline def copyAsciiStringToBytes(s: String, dst: Array[Byte], dstPos: Int): Unit = {
var i = 0
var pos = dstPos
val len = s.length
while (i < len) {
dst(pos) = s.charAt(i).toByte
i += 1
pos += 1
}
}

private def nodeToJson(node: Node): ujson.Value = node match {
case _: Node.ScalarNode =>
YamlDecoder.forAny.construct(node).getOrElse("") match {
Expand Down
7 changes: 7 additions & 0 deletions sjsonnet/src-jvm/sjsonnet/Platform.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import org.tukaani.xz.XZOutputStream
import org.yaml.snakeyaml.{LoaderOptions, Yaml}
import org.yaml.snakeyaml.constructor.SafeConstructor

import scala.annotation.nowarn
import scala.collection.compat.*
import scala.collection.mutable
import scala.jdk.CollectionConverters.*
Expand All @@ -23,6 +24,12 @@ object Platform {
def repeatString(s: String, count: Int): String =
if (count <= 0) "" else s.repeat(count)

// Used only for strings already proven ASCII-safe; copies low bytes without allocation.
@inline
@nowarn("cat=deprecation")
def copyAsciiStringToBytes(s: String, dst: Array[Byte], dstPos: Int): Unit =
s.getBytes(0, s.length, dst, dstPos)

def gzipBytes(b: Array[Byte]): String = {
val outputStream: ByteArrayOutputStream = new ByteArrayOutputStream(b.length)
val gzip: GZIPOutputStream = new GZIPOutputStream(outputStream)
Expand Down
7 changes: 7 additions & 0 deletions sjsonnet/src-native/sjsonnet/Platform.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import java.util
import java.util.Base64
import java.util.zip.GZIPOutputStream
import scala.scalanative.regex.Pattern
import scala.annotation.nowarn
import scala.collection.mutable
import org.virtuslab.yaml.*

Expand All @@ -28,6 +29,12 @@ object Platform {
}
}

// Used only for strings already proven ASCII-safe; copies low bytes without allocation.
@inline
@nowarn("cat=deprecation")
def copyAsciiStringToBytes(s: String, dst: Array[Byte], dstPos: Int): Unit =
s.getBytes(0, s.length, dst, dstPos)

def gzipBytes(b: Array[Byte]): String = {
val outputStream: ByteArrayOutputStream = new ByteArrayOutputStream(b.length)
val gzip: GZIPOutputStream = new GZIPOutputStream(outputStream)
Expand Down
8 changes: 2 additions & 6 deletions sjsonnet/src/sjsonnet/BaseByteRenderer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -253,12 +253,8 @@ class BaseByteRenderer[T <: java.io.OutputStream](
var pos = elemBuilder.length
arr(pos) = '"'.toByte
pos += 1
var i = 0
while (i < len) {
arr(pos) = str.charAt(i).toByte
pos += 1
i += 1
}
Platform.copyAsciiStringToBytes(str, arr, pos)
pos += len
arr(pos) = '"'.toByte
elemBuilder.length = pos + 1
}
Expand Down
17 changes: 15 additions & 2 deletions sjsonnet/src/sjsonnet/stdlib/EncodingModule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ import sjsonnet.functions.AbstractFunctionModule
object EncodingModule extends AbstractFunctionModule {
def name = "encoding"

@inline private def isAsciiJsonSafe(bytes: Array[Byte]): Boolean = {
var i = 0
while (i < bytes.length) {
val b = bytes(i) & 0xff
if (b < 0x20 || b >= 0x80 || b == '"' || b == '\\') return false
i += 1
}
true
}

/**
* [[https://jsonnet.org/ref/stdlib.html#std-md5 std.md5(s)]].
*
Expand Down Expand Up @@ -88,9 +98,12 @@ object EncodingModule extends AbstractFunctionModule {
* Behaves like std.base64DecodeBytes() except returns a naively encoded string instead of an
* array of bytes.
*/
builtin("base64Decode", "str") { (_, _, str: String) =>
builtin("base64Decode", "str") { (pos, _, str: String) =>
try {
new String(PlatformBase64.decode(str), UTF_8)
val decoded = PlatformBase64.decode(str)
val value = new String(decoded, UTF_8)
(if (isAsciiJsonSafe(decoded)) Val.Str.asciiSafe(pos, value)
else Val.Str(pos, value)): Val
} catch {
case e: IllegalArgumentException =>
Error.fail("Invalid base64 string: " + e.getMessage)
Expand Down
Loading