diff --git a/sjsonnet/src/sjsonnet/stdlib/Base64Validation.scala b/sjsonnet/src/sjsonnet/stdlib/Base64Validation.scala index 2fdb8893..73383815 100644 --- a/sjsonnet/src/sjsonnet/stdlib/Base64Validation.scala +++ b/sjsonnet/src/sjsonnet/stdlib/Base64Validation.scala @@ -6,6 +6,20 @@ package sjsonnet.stdlib */ object Base64Validation { + def hasStrictPaddingLengthError(input: String): Boolean = { + val len = input.length + if (len == 0 || len % 4 == 0) false + else { + var allAscii = true + var i = 0 + while (i < len && allAscii) { + if (input.charAt(i) > 127) allAscii = false + i += 1 + } + allAscii + } + } + /** * Validate strict RFC 4648 padding: reject ASCII input whose length is not a multiple of 4. * @@ -20,19 +34,10 @@ object Base64Validation { * bytes (so "ĀQ=" is 4 bytes, passes the length check, and fails on the invalid character). */ def requireStrictPadding(input: String): Unit = { - val len = input.length - if (len > 0 && len % 4 != 0) { - var allAscii = true - var i = 0 - while (i < len && allAscii) { - if (input.charAt(i) > 127) allAscii = false - i += 1 - } - if (allAscii) { - throw new IllegalArgumentException( - "Last unit does not have enough valid bits" - ) - } + if (hasStrictPaddingLengthError(input)) { + throw new IllegalArgumentException( + "Last unit does not have enough valid bits" + ) } } } diff --git a/sjsonnet/src/sjsonnet/stdlib/EncodingModule.scala b/sjsonnet/src/sjsonnet/stdlib/EncodingModule.scala index 68e80308..e5810712 100644 --- a/sjsonnet/src/sjsonnet/stdlib/EncodingModule.scala +++ b/sjsonnet/src/sjsonnet/stdlib/EncodingModule.scala @@ -118,7 +118,12 @@ object EncodingModule extends AbstractFunctionModule { else Val.Str(pos, value)): Val } catch { case e: IllegalArgumentException => - Error.fail("Invalid base64 string: " + e.getMessage) + if (Base64Validation.hasStrictPaddingLengthError(str)) + Error.fail( + s"input string appears not to be a base64 encoded string. Wrong length found (${str.length})" + ) + else + Error.fail("failed to decode: " + e.getMessage) } }, /** @@ -135,7 +140,12 @@ object EncodingModule extends AbstractFunctionModule { Val.Arr.fromBytes(pos, PlatformBase64.decode(str)) } catch { case e: IllegalArgumentException => - Error.fail("Invalid base64 string: " + e.getMessage) + if (Base64Validation.hasStrictPaddingLengthError(str)) + Error.fail( + s"input string appears not to be a base64 encoded string. Wrong length found (${str.length})" + ) + else + Error.fail("failed to decode: " + e.getMessage) } }, /** diff --git a/sjsonnet/src/sjsonnet/stdlib/StringModule.scala b/sjsonnet/src/sjsonnet/stdlib/StringModule.scala index fb8d818d..5c2e1976 100644 --- a/sjsonnet/src/sjsonnet/stdlib/StringModule.scala +++ b/sjsonnet/src/sjsonnet/stdlib/StringModule.scala @@ -1225,11 +1225,17 @@ object StringModule extends AbstractFunctionModule { */ private object DecodeUTF8 extends Val.Builtin1("decodeUTF8", "arr") { def evalRhs(arr: Eval, ev: EvalScope, pos: Position): Val = { - for ((v, idx) <- arr.value.asArr.iterator.zipWithIndex) { - if (!v.isInstanceOf[Val.Num] || !v.asDouble.isWhole || v.asInt < 0 || v.asInt > 255) { - throw Error.fail( - "std.decodeUTF8: element " + idx + " is not an integer in range [0,255], got " + v.value.prettyName - ) + for (v <- arr.value.asArr.iterator) { + v match { + case n: Val.Num => + val d = n.asDouble + if (!d.isWhole) + throw Error.fail("Expected an integer, but got " + d) + val i = d.toInt + if (i < 0 || i > 255) + throw Error.fail("Bytes must be integers in range [0, 255], got " + i) + case _ => + throw Error.fail("Unexpected type " + v.value.prettyName + ", expected number") } } Val.Str( diff --git a/sjsonnet/test/resources/go_test_suite/builtinBase64DecodeBytes_high_codepoint.jsonnet.golden b/sjsonnet/test/resources/go_test_suite/builtinBase64DecodeBytes_high_codepoint.jsonnet.golden index 935593b7..d988645e 100644 --- a/sjsonnet/test/resources/go_test_suite/builtinBase64DecodeBytes_high_codepoint.jsonnet.golden +++ b/sjsonnet/test/resources/go_test_suite/builtinBase64DecodeBytes_high_codepoint.jsonnet.golden @@ -1,3 +1,2 @@ -sjsonnet.Error: [std.base64DecodeBytes] Invalid base64 string: Illegal base64 character 3f +sjsonnet.Error: [std.base64DecodeBytes] failed to decode: Illegal base64 character 3f at [].(builtinBase64DecodeBytes_high_codepoint.jsonnet:1:22) - diff --git a/sjsonnet/test/resources/go_test_suite/builtinBase64DecodeBytes_invalid_base64_data.jsonnet.golden b/sjsonnet/test/resources/go_test_suite/builtinBase64DecodeBytes_invalid_base64_data.jsonnet.golden index 841b374b..6740e3d1 100644 --- a/sjsonnet/test/resources/go_test_suite/builtinBase64DecodeBytes_invalid_base64_data.jsonnet.golden +++ b/sjsonnet/test/resources/go_test_suite/builtinBase64DecodeBytes_invalid_base64_data.jsonnet.golden @@ -1,3 +1,2 @@ -sjsonnet.Error: [std.base64DecodeBytes] Invalid base64 string: Last unit does not have enough valid bits +sjsonnet.Error: [std.base64DecodeBytes] input string appears not to be a base64 encoded string. Wrong length found (5) at [].(builtinBase64DecodeBytes_invalid_base64_data.jsonnet:1:22) - diff --git a/sjsonnet/test/resources/go_test_suite/builtinBase64Decode_high_codepoint.jsonnet.golden b/sjsonnet/test/resources/go_test_suite/builtinBase64Decode_high_codepoint.jsonnet.golden index dcaff766..79c95960 100644 --- a/sjsonnet/test/resources/go_test_suite/builtinBase64Decode_high_codepoint.jsonnet.golden +++ b/sjsonnet/test/resources/go_test_suite/builtinBase64Decode_high_codepoint.jsonnet.golden @@ -1,3 +1,2 @@ -sjsonnet.Error: [std.base64Decode] Invalid base64 string: Illegal base64 character 3f +sjsonnet.Error: [std.base64Decode] failed to decode: Illegal base64 character 3f at [].(builtinBase64Decode_high_codepoint.jsonnet:1:17) - diff --git a/sjsonnet/test/resources/go_test_suite/builtinBase64Decode_invalid_base64_data.jsonnet.golden b/sjsonnet/test/resources/go_test_suite/builtinBase64Decode_invalid_base64_data.jsonnet.golden index d407788f..ca32ace3 100644 --- a/sjsonnet/test/resources/go_test_suite/builtinBase64Decode_invalid_base64_data.jsonnet.golden +++ b/sjsonnet/test/resources/go_test_suite/builtinBase64Decode_invalid_base64_data.jsonnet.golden @@ -1,3 +1,2 @@ -sjsonnet.Error: [std.base64Decode] Invalid base64 string: Last unit does not have enough valid bits +sjsonnet.Error: [std.base64Decode] input string appears not to be a base64 encoded string. Wrong length found (5) at [].(builtinBase64Decode_invalid_base64_data.jsonnet:1:17) - diff --git a/sjsonnet/test/resources/test_suite/error.decodeUTF8_float.jsonnet.golden b/sjsonnet/test/resources/test_suite/error.decodeUTF8_float.jsonnet.golden index 082d2fb5..be92a8b5 100644 --- a/sjsonnet/test/resources/test_suite/error.decodeUTF8_float.jsonnet.golden +++ b/sjsonnet/test/resources/test_suite/error.decodeUTF8_float.jsonnet.golden @@ -1,2 +1,2 @@ -sjsonnet.Error: [std.decodeUTF8] element 0 is not an integer in range [0,255], got number +sjsonnet.Error: [std.decodeUTF8] Expected an integer, but got 17.5 at [].(error.decodeUTF8_float.jsonnet:1:15) diff --git a/sjsonnet/test/resources/test_suite/error.decodeUTF8_nan.jsonnet.golden b/sjsonnet/test/resources/test_suite/error.decodeUTF8_nan.jsonnet.golden index 03fdbfeb..b47efc6f 100644 --- a/sjsonnet/test/resources/test_suite/error.decodeUTF8_nan.jsonnet.golden +++ b/sjsonnet/test/resources/test_suite/error.decodeUTF8_nan.jsonnet.golden @@ -1,2 +1,2 @@ -sjsonnet.Error: [std.decodeUTF8] element 0 is not an integer in range [0,255], got string +sjsonnet.Error: [std.decodeUTF8] Unexpected type string, expected number at [].(error.decodeUTF8_nan.jsonnet:1:15) diff --git a/sjsonnet/test/src/sjsonnet/Base64Tests.scala b/sjsonnet/test/src/sjsonnet/Base64Tests.scala index 70a69f31..dc6c33cf 100644 --- a/sjsonnet/test/src/sjsonnet/Base64Tests.scala +++ b/sjsonnet/test/src/sjsonnet/Base64Tests.scala @@ -461,9 +461,21 @@ object Base64Tests extends TestSuite { // Error handling // ================================================================ test("errors") { + val highCodepointBase64 = 0x0100.toChar.toString + "Q=" + test("invalidBase64Char") { val err = evalErr("""std.base64Decode("!!!!")""") - assert(err.contains("Invalid base64")) + assert(err.contains("failed to decode")) + } + test("highCodepointInvalidChar") { + val err = evalErr("std.base64Decode(\"" + highCodepointBase64 + "\")") + assert(err.contains("failed to decode")) + assert(!err.contains("Wrong length found")) + } + test("decodeBytesHighCodepointInvalidChar") { + val err = evalErr("std.base64DecodeBytes(\"" + highCodepointBase64 + "\")") + assert(err.contains("failed to decode")) + assert(!err.contains("Wrong length found")) } test("invalidByteArrayValue") { val err = evalErr("""std.base64([256])""") @@ -502,22 +514,22 @@ object Base64Tests extends TestSuite { test("missingTwoPads") { // "YQ" should be "YQ==" — both go-jsonnet and C++ jsonnet reject this val err = evalErr("""std.base64Decode("YQ")""") - assert(err.contains("Invalid base64")) + assert(err.contains("appears not to be a base64 encoded string")) } test("missingOnePad") { // "YWI" should be "YWI=" — rejected by both official implementations val err = evalErr("""std.base64Decode("YWI")""") - assert(err.contains("Invalid base64")) + assert(err.contains("appears not to be a base64 encoded string")) } test("singleChar") { // Single character is never valid base64 val err = evalErr("""std.base64Decode("A")""") - assert(err.contains("Invalid base64")) + assert(err.contains("appears not to be a base64 encoded string")) } test("fiveChars") { // "wrong" (5 chars) — the go_test_suite canonical test case val err = evalErr("""std.base64Decode("wrong")""") - assert(err.contains("Invalid base64")) + assert(err.contains("appears not to be a base64 encoded string")) } test("validPaddedStillWorks") { // Properly padded input must still work @@ -536,7 +548,7 @@ object Base64Tests extends TestSuite { // DecodeBytes also enforces strict padding test("decodeBytesUnpadded") { val err = evalErr("""std.base64DecodeBytes("YQ")""") - assert(err.contains("Invalid base64")) + assert(err.contains("appears not to be a base64 encoded string")) } }