diff --git a/runtime/pb/json.lua b/runtime/pb/json.lua index f1838ca00b5eeeebc48d0a190e74703b7ef14994..e6a2ea6670f452c0cc5accd907521bb25bafac9d 100644 --- a/runtime/pb/json.lua +++ b/runtime/pb/json.lua @@ -573,7 +573,7 @@ if type(v) == 'number' then return string.format('%.0f', v) end return tostring(v) end if proto_type == 'uint32' then return v end - if proto_type == 'bytes' then return digest.base64_encode(v) end + if proto_type == 'bytes' then return digest.base64_encode(v, {nowrap = true}) end if proto_type == 'float' or proto_type == 'double' then return v -- handled by encode_json_number end @@ -740,7 +740,7 @@ -- keeps round-trips through user-registered types stable without -- forcing every embedded payload into a Wkt shape). local obj = {} if type_url ~= '' then obj['@type'] = type_url end - if bytes ~= '' then obj.value = digest.base64_encode(bytes) end + if bytes ~= '' then obj.value = digest.base64_encode(bytes, {nowrap = true}) end return obj end local inner = desc.decode and desc.decode(bytes) diff --git a/test/any_fieldmask_test.lua b/test/any_fieldmask_test.lua index af0ad602eef6305ce04ca009c51b2615f01d67fd..696c7dc3dee68ef190ccad4c3f903b63437e47da 100644 --- a/test/any_fieldmask_test.lua +++ b/test/any_fieldmask_test.lua @@ -127,6 +127,17 @@ t.assert_equals(back.extension.type_url, 'type.opaque/Foo') t.assert_equals(back.extension.value, '\x01\x02\x03') end +gaj.test_json_opaque_value_base64_unwrapped = function() + -- Same canonical-base64 rule as plain bytes fields: the Any opaque + -- fallback must not emit MIME-style line wraps inside the JSON string. + local opaque = {type_url = 'type.opaque/Big', value = string.rep('A', 64)} + local enc = pb.json.encode(hello.Event_descriptor, {extension = opaque}) + t.assert_not_str_contains(enc, '\n') + t.assert_not_str_contains(enc, '\\n') + local back = pb.json.decode(hello.Event_descriptor, enc) + t.assert_equals(back.extension.value, opaque.value) +end + -- --------------------------------------------------------------------------- -- End-to-end: Event message round-trip with extension + update_mask. -- --------------------------------------------------------------------------- diff --git a/test/json_test.lua b/test/json_test.lua index e59045702bad5890a1d61ca238190686f81f5afa..50d3b0755891491bba40dddde731fe005332a16e 100644 --- a/test/json_test.lua +++ b/test/json_test.lua @@ -43,6 +43,21 @@ local p2 = pb.json.decode(hello.Person_descriptor, pb.json.encode(hello.Person_descriptor, p)) t.assert_equals(p2.avatar, p.avatar) end +g.test_bytes_base64_unwrapped_long_payload = function() + -- Canonical proto3 JSON requires RFC 4648 base64 with no line wrapping. + -- Tarantool's digest.base64_encode defaults to MIME-style 76-char wrap, + -- so any payload past ~57 bytes used to emit a `\n` inside the JSON + -- string and break grpc-gateway / protojson consumers. + local p = {avatar = string.rep('A', 64)} + local enc = pb.json.encode(hello.Person_descriptor, p) + t.assert_not_str_contains(enc, '\n', 'no raw newline anywhere in JSON output') + t.assert_not_str_contains(enc, '\\n', 'no escaped newline in base64 token') + local obj = reparse(enc) + t.assert_not_str_contains(obj.avatar, '\n', 'base64 token is a single line') + local p2 = pb.json.decode(hello.Person_descriptor, enc) + t.assert_equals(p2.avatar, p.avatar) +end + g.test_repeated_packed_scalar = function() local p = {lucky_numbers = {1, 2, 3}} local obj = reparse(pb.json.encode(hello.Person_descriptor, p))