diff --git a/readme.md b/readme.md index 9981edc6..f1fed13c 100644 --- a/readme.md +++ b/readme.md @@ -280,6 +280,10 @@ to ensure the output bytecode remains compatible with users on older JVMs. ## Changelog +### 0.4.11 +- Implement `std.isEmpty`, `std.xor`, `std.xnor`, `std.trim`, + `std.equalsIgnoreCase`, `std.sha1`, `std.sha256`, `std.sha512`, `std.sha3` + ### 0.4.10 - Implement `std.get` [#202](https://github.com/databricks/sjsonnet/pull/202), diff --git a/sjsonnet/src-js/sjsonnet/Platform.scala b/sjsonnet/src-js/sjsonnet/Platform.scala index 81805fb7..45c2ede3 100644 --- a/sjsonnet/src-js/sjsonnet/Platform.scala +++ b/sjsonnet/src-js/sjsonnet/Platform.scala @@ -19,6 +19,18 @@ object Platform { def md5(s: String): String = { throw new Exception("MD5 not implemented in Scala.js") } + def sha1(s: String): String = { + throw new Exception("SHA1 not implemented in Scala.js") + } + def sha256(s: String): String = { + throw new Exception("SHA256 not implemented in Scala.js") + } + def sha512(s: String): String = { + throw new Exception("SHA512 not implemented in Scala.js") + } + def sha3(s: String): String = { + throw new Exception("SHA3 not implemented in Scala.js") + } def hashFile(file: File): String = { throw new Exception("hashFile not implemented in Scala.js") } diff --git a/sjsonnet/src-jvm/sjsonnet/Platform.scala b/sjsonnet/src-jvm/sjsonnet/Platform.scala index c56af2a5..c94d867c 100644 --- a/sjsonnet/src-jvm/sjsonnet/Platform.scala +++ b/sjsonnet/src-jvm/sjsonnet/Platform.scala @@ -15,11 +15,13 @@ object Platform { def gzipBytes(b: Array[Byte]): String = { val outputStream: ByteArrayOutputStream = new ByteArrayOutputStream(b.length) val gzip: GZIPOutputStream = new GZIPOutputStream(outputStream) - gzip.write(b) - gzip.close() - val gzippedBase64: String = Base64.getEncoder.encodeToString(outputStream.toByteArray) - outputStream.close() - gzippedBase64 + try { + gzip.write(b) + Base64.getEncoder.encodeToString(outputStream.toByteArray) + } finally { + gzip.close() + outputStream.close() + } } def gzipString(s: String): String = { gzipBytes(s.getBytes()) @@ -33,11 +35,13 @@ object Platform { // Set compression to specified level val level = compressionLevel.getOrElse(LZMA2Options.PRESET_DEFAULT) val xz: XZOutputStream = new XZOutputStream(outputStream, new LZMA2Options(level)) - xz.write(b) - xz.close() - val xzedBase64: String = Base64.getEncoder.encodeToString(outputStream.toByteArray) - outputStream.close() - xzedBase64 + try { + xz.write(b) + Base64.getEncoder.encodeToString(outputStream.toByteArray) + } finally { + xz.close() + outputStream.close() + } } def xzString(s: String, compressionLevel: Option[Int]): String = { @@ -48,13 +52,25 @@ object Platform { val yaml: java.util.LinkedHashMap[String, Object] = new Yaml(new Constructor(classOf[java.util.LinkedHashMap[String, Object]])).load(yamlString) new JSONObject(yaml).toString() } - def md5(s: String): String = { - java.security.MessageDigest.getInstance("MD5") + + private def computeHash(algorithm: String, s: String) = { + java.security.MessageDigest.getInstance(algorithm) .digest(s.getBytes("UTF-8")) - .map{ b => String.format("%02x", new java.lang.Integer(b & 0xff))} + .map{ b => String.format("%02x", (b & 0xff).asInstanceOf[Integer])} .mkString } + def md5(s: String): String = computeHash("MD5", s) + + def sha1(s: String): String = computeHash("SHA-1", s) + + def sha256(s: String): String = computeHash("SHA-256", s) + + def sha512(s: String): String = computeHash("SHA-512", s) + + // Same as go-jsonnet https://github.com/google/go-jsonnet/blob/2b4d7535f540f128e38830492e509a550eb86d57/builtins.go#L959 + def sha3(s: String): String = computeHash("SHA3-512", s) + private[this] val xxHashFactory = XXHashFactory.fastestInstance() def hashFile(file: File): String = { @@ -75,6 +91,6 @@ object Platform { fis.close() } - hash.getValue().toString + hash.getValue.toString } } diff --git a/sjsonnet/src-native/sjsonnet/Platform.scala b/sjsonnet/src-native/sjsonnet/Platform.scala index 8e9c7d3e..0a88ce0f 100644 --- a/sjsonnet/src-native/sjsonnet/Platform.scala +++ b/sjsonnet/src-native/sjsonnet/Platform.scala @@ -19,6 +19,18 @@ object Platform { def md5(s: String): String = { throw new Exception("MD5 not implemented in Scala Native") } + def sha1(s: String): String = { + throw new Exception("SHA1 not implemented in Scala Native") + } + def sha256(s: String): String = { + throw new Exception("SHA256 not implemented in Scala Native") + } + def sha512(s: String): String = { + throw new Exception("SHA512 not implemented in Scala Native") + } + def sha3(s: String): String = { + throw new Exception("SHA3 not implemented in Scala Native") + } def hashFile(file: File): String = { // File hashes in Scala Native are just the file content diff --git a/sjsonnet/src/sjsonnet/Std.scala b/sjsonnet/src/sjsonnet/Std.scala index 9c79d0cc..544bacf8 100644 --- a/sjsonnet/src/sjsonnet/Std.scala +++ b/sjsonnet/src/sjsonnet/Std.scala @@ -1253,9 +1253,36 @@ class Std { } }, "all" -> All, - "any" -> Any + "any" -> Any, + builtin("isEmpty", "str") { (_, _, str: String) => + str.isEmpty + }, + builtin("trim", "str") { (_, _, str: String) => + str.trim + }, + builtin("equalsIgnoreCase", "str1", "str2") { (_, _, str1: String, str2: String) => + str1.equalsIgnoreCase(str2) + }, + builtin("xor", "bool1", "bool2") { (_, _, bool1: Boolean, bool2: Boolean) => + bool1 ^ bool2 + }, + builtin("xnor", "bool1", "bool2") { (_, _, bool1: Boolean, bool2: Boolean) => + !(bool1 ^ bool2) + }, + builtin("sha1", "str") { (_, _, str: String) => + Platform.sha1(str) + }, + builtin("sha256", "str") { (_, _, str: String) => + Platform.sha256(str) + }, + builtin("sha512", "str") { (_, _, str: String) => + Platform.sha512(str) + }, + builtin("sha3", "str") { (_, _, str: String) => + Platform.sha3(str) + }, ) - val Std = Val.Obj.mk( + val Std: Val.Obj = Val.Obj.mk( null, functions.toSeq .map{ @@ -1382,7 +1409,6 @@ class Std { case vs: Val.Arr => new Val.Arr( pos, - if (vs.forall(_.isInstanceOf[Val.Str])){ vs.asStrictArray.map(_.cast[Val.Str]).sortBy(_.value) }else if (vs.forall(_.isInstanceOf[Val.Num])) { diff --git a/sjsonnet/test/src-jvm/sjsonnet/StdShasTests.scala b/sjsonnet/test/src-jvm/sjsonnet/StdShasTests.scala new file mode 100644 index 00000000..e300e485 --- /dev/null +++ b/sjsonnet/test/src-jvm/sjsonnet/StdShasTests.scala @@ -0,0 +1,22 @@ +package sjsonnet + +import sjsonnet.TestUtils.eval +import utest._ + +object StdShasTests extends TestSuite { + + def tests: Tests = Tests { + test { + eval("std.sha1('')") ==> ujson.Str("da39a3ee5e6b4b0d3255bfef95601890afd80709") + eval("std.sha256('')") ==> ujson.Str("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") + eval("std.sha512('')") ==> ujson.Str("cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e") + eval("std.sha3('')") ==> ujson.Str("a69f73cca23a9ac5c8b567dc185a756e97c982164fe25859e0d1dcc1475c80a615b2123af1f5f94c11e3e9402c3ac558f500199d95b6d3e301758586281dcd26") + } + test { + eval("std.sha1('foo')") ==> ujson.Str("0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33") + eval("std.sha256('foo')") ==> ujson.Str("2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae") + eval("std.sha512('foo')") ==> ujson.Str("f7fbba6e0636f890e56fbbf3283e524c6fa3204ae298382d624741d0dc6638326e282c41be5e4254d8820772c5518a2c5a8c0c7f7eda19594a7eb539453e1ed7") + eval("std.sha3('foo')") ==> ujson.Str("4bca2b137edc580fe50a88983ef860ebaca36c857b1f492839d6d7392452a63c82cbebc68e3b70a2a1480b4bb5d437a7cba6ecf9d89f9ff3ccd14cd6146ea7e7") + } + } +} diff --git a/sjsonnet/test/src/sjsonnet/Std0150FunctionsTests.scala b/sjsonnet/test/src/sjsonnet/Std0150FunctionsTests.scala index f401a372..6dfc1e85 100644 --- a/sjsonnet/test/src/sjsonnet/Std0150FunctionsTests.scala +++ b/sjsonnet/test/src/sjsonnet/Std0150FunctionsTests.scala @@ -1,7 +1,7 @@ package sjsonnet import utest._ -import TestUtils.eval +import TestUtils.{eval, evalErr} object Std0150FunctionsTests extends TestSuite { def tests = Tests { @@ -180,5 +180,46 @@ object Std0150FunctionsTests extends TestSuite { eval("""std.all([false, true, false])""") ==> ujson.Bool(false) eval("""std.all([false, false, false])""") ==> ujson.Bool(false) } + + test("isEmpty") { + eval("""std.isEmpty("")""") ==> ujson.Bool(true) + eval("""std.isEmpty("non-empty string")""") ==> ujson.Bool(false) + assert( + evalErr("""std.isEmpty(10)""") + .startsWith("sjsonnet.Error: Wrong parameter type: expected String, got number") + ) + } + + test("trim") { + eval("""std.trim("already trimmed string")""") ==> ujson.Str("already trimmed string") + eval("""std.trim(" string with spaces on both ends ")""") ==> ujson.Str("string with spaces on both ends") + eval("""std.trim("string with newline character at end\n")""") ==> ujson.Str("string with newline character at end") + eval("""std.trim("string with tabs at end\t\t")""") ==> ujson.Str("string with tabs at end") + assert( + evalErr("""std.trim(10)""").startsWith("sjsonnet.Error: Wrong parameter type: expected String, got number")) + } + + test("xnor") { + eval("""std.xnor(false, true)""") ==> ujson.False + eval("""std.xnor(false, false)""") ==> ujson.True + assert( + evalErr("""std.xnor("false", false)""") + .startsWith("sjsonnet.Error: Wrong parameter type: expected Boolean, got string") + ) + } + + test("xor") { + eval("""std.xor(false, true)""") ==> ujson.True + eval("""std.xor(true, true)""") ==> ujson.False + assert( + evalErr("""std.xor("false", false)""") + .startsWith("sjsonnet.Error: Wrong parameter type: expected Boolean, got string") + ) + } + + test("equalsIgnoreCase") { + eval("""std.equalsIgnoreCase("hello", "HELLO")""") ==> ujson.True + eval("""std.equalsIgnoreCase("hello", "world")""") ==> ujson.False + } } }