diff --git a/.changes/next-release/bugfix-Formatter-25003.json b/.changes/next-release/bugfix-Formatter-25003.json new file mode 100644 index 000000000000..a1cd65c63421 --- /dev/null +++ b/.changes/next-release/bugfix-Formatter-25003.json @@ -0,0 +1,5 @@ +{ + "type": "bugfix", + "category": "Formatter", + "description": "Update JSON formatter to encode raw bytes as UTF-8." +} diff --git a/awscli/utils.py b/awscli/utils.py index 995b40473de0..68ed4415ca92 100644 --- a/awscli/utils.py +++ b/awscli/utils.py @@ -10,6 +10,7 @@ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. +import base64 import contextlib import csv import datetime @@ -185,11 +186,14 @@ def operation_uses_document_types(operation_model): def json_encoder(obj): - """JSON encoder that formats datetimes as ISO8601 format.""" + """JSON encoder that formats datetimes as ISO8601 format + and encodes bytes to UTF-8 Base64 string.""" if isinstance(obj, datetime.datetime): return obj.isoformat() + elif isinstance(obj, bytes): + return base64.b64encode(obj).decode("utf-8") else: - return obj + raise TypeError('Encountered unrecognized type in JSON encoder.') @contextlib.contextmanager diff --git a/tests/unit/output/test_json_output.py b/tests/unit/output/test_json_output.py index 826380ec3453..8e3f91f46bc4 100644 --- a/tests/unit/output/test_json_output.py +++ b/tests/unit/output/test_json_output.py @@ -11,6 +11,10 @@ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. +import base64 +import contextlib +import io +import sys from botocore.compat import json import platform from awscli.formatter import JSONFormatter @@ -127,3 +131,26 @@ def test_fully_buffered_handles_io_error(self): # we still should have called the flush() on the # stream. fake_closed_stream.flush.assert_called_with() + + +class TestBinaryData(unittest.TestCase): + def test_binary_data_gets_base64_encoded(self): + args = mock.Mock(query=None) + raw_bytes = b'foo' + response = {'BinaryValue': raw_bytes} + stdout_b = io.BytesIO() + stdout = io.TextIOWrapper(stdout_b, newline='\n') + formatter = JSONFormatter(args) + + with contextlib.redirect_stdout(stdout): + formatter('command-name', response, sys.stdout) + stdout.flush() + + assert ( + stdout_b.getvalue() + == ( + '{\n' + f' "BinaryValue": "{base64.b64encode(raw_bytes).decode("utf-8")}"\n' + '}\n' + ).encode() + )