From af79b3ad39b3383a28f83d84e67bd7fe29d051f5 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Thu, 3 Aug 2023 14:32:39 +1200 Subject: [PATCH] Encode serialized output to avoid encoding errors. (#44) --- lib/console/output/encoder.rb | 40 ++++++++++++++++++++++++++++++++++ lib/console/output/json.rb | 6 ++++- test/console/output.rb | 18 ++++++++++++++- test/console/output/encoder.rb | 40 ++++++++++++++++++++++++++++++++++ 4 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 lib/console/output/encoder.rb create mode 100644 test/console/output/encoder.rb diff --git a/lib/console/output/encoder.rb b/lib/console/output/encoder.rb new file mode 100644 index 0000000..ba49953 --- /dev/null +++ b/lib/console/output/encoder.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2023, by Samuel Williams. + +module Console + module Output + class Encoder + def initialize(output, encoding = ::Encoding::UTF_8) + @output = output + @encoding = encoding + end + + attr :output + + attr :encoding + + def call(subject = nil, *arguments, **options, &block) + subject = encode(subject) + arguments = encode(arguments) + options = encode(options) + + @output.call(subject, *arguments, **options, &block) + end + + def encode(value) + case value + when String + value.encode(@encoding, invalid: :replace, undef: :replace) + when Array + value.map{|item| encode(item)} + when Hash + value.transform_values{|item| encode(item)} + else + value + end + end + end + end +end diff --git a/lib/console/output/json.rb b/lib/console/output/json.rb index da2e84f..b9a9043 100644 --- a/lib/console/output/json.rb +++ b/lib/console/output/json.rb @@ -4,12 +4,16 @@ # Copyright, 2021-2022, by Samuel Williams. require_relative '../serialized/logger' +require_relative 'encoder' module Console module Output module JSON def self.new(output, **options) - Serialized::Logger.new(output, format: ::JSON, **options) + # The output encoder can prevent encoding issues (e.g. invalid UTF-8): + Output::Encoder.new( + Serialized::Logger.new(output, format: ::JSON, **options) + ) end end end diff --git a/test/console/output.rb b/test/console/output.rb index 12829ec..6acaf94 100644 --- a/test/console/output.rb +++ b/test/console/output.rb @@ -14,7 +14,7 @@ let(:output) {File.open('/tmp/console.log', 'w')} it 'should use a serialized format' do - expect(Console::Output.new(output, env)).to be_a(Console::Serialized::Logger) + expect(Console::Output.new(output, env).output).to be_a(Console::Serialized::Logger) end end @@ -31,7 +31,9 @@ it 'can set output to Serialized and format to JSON by ENV' do output = Console::Output.new(StringIO.new, {'CONSOLE_OUTPUT' => 'JSON'}) + expect(output).to be_a(Console::Output::Encoder) + output = output.output expect(output).to be_a Console::Serialized::Logger expect(output.format).to be == JSON end @@ -64,4 +66,18 @@ expect(output.terminal).to be_a Console::Terminal::Text end end + + with "invalid UTF-8" do + let(:capture) {StringIO.new} + + it "should replace invalid characters" do + expect(capture).to receive(:tty?).and_return(false) + output = Console::Output.new(capture, {}) + + output.call("Hello \xFF") + + message = JSON.parse(capture.string) + expect(message['subject']).to be == "Hello \uFFFD" + end + end end diff --git a/test/console/output/encoder.rb b/test/console/output/encoder.rb new file mode 100644 index 0000000..4262bdb --- /dev/null +++ b/test/console/output/encoder.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2023, by Samuel Williams. + +require 'console/output/encoder' +require 'console/capture' + +describe Console::Output::Encoder do + let(:output) {Console::Capture.new} + let(:encoder) {subject.new(output)} + + let(:invalid_string) {"hello \xc3\x28 world"} + it "is an invalid string" do + expect(invalid_string).not.to be(:valid_encoding?) + end + + it "can fix encoding" do + valid_string = encoder.encode(invalid_string) + expect(valid_string).to be(:valid_encoding?) + end + + it "can encode hashes" do + invalid = {key: invalid_string} + valid = encoder.encode(invalid) + + expect(valid[:key]).to be(:valid_encoding?) + end + + it "can encode arrays" do + invalid = [invalid_string] + valid = encoder.encode(invalid) + + expect(valid.first).to be(:valid_encoding?) + end + + it "ignores non-strings" do + expect(encoder.encode(1)).to be == 1 + end +end