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
31 changes: 18 additions & 13 deletions sjsonnet/src/sjsonnet/stdlib/Base64Validation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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"
)
}
}
}
14 changes: 12 additions & 2 deletions sjsonnet/src/sjsonnet/stdlib/EncodingModule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
},
/**
Expand All @@ -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)
}
},
/**
Expand Down
16 changes: 11 additions & 5 deletions sjsonnet/src/sjsonnet/stdlib/StringModule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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 [<root>].(builtinBase64DecodeBytes_high_codepoint.jsonnet:1:22)

Original file line number Diff line number Diff line change
@@ -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 [<root>].(builtinBase64DecodeBytes_invalid_base64_data.jsonnet:1:22)

Original file line number Diff line number Diff line change
@@ -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 [<root>].(builtinBase64Decode_high_codepoint.jsonnet:1:17)

Original file line number Diff line number Diff line change
@@ -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 [<root>].(builtinBase64Decode_invalid_base64_data.jsonnet:1:17)

Original file line number Diff line number Diff line change
@@ -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 [<root>].(error.decodeUTF8_float.jsonnet:1:15)
Original file line number Diff line number Diff line change
@@ -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 [<root>].(error.decodeUTF8_nan.jsonnet:1:15)
24 changes: 18 additions & 6 deletions sjsonnet/test/src/sjsonnet/Base64Tests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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])""")
Expand Down Expand Up @@ -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
Expand All @@ -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"))
}
}

Expand Down
Loading