diff --git a/lib/gpgme/io_callbacks.rb b/lib/gpgme/io_callbacks.rb index 4dbc4bb..79b2288 100644 --- a/lib/gpgme/io_callbacks.rb +++ b/lib/gpgme/io_callbacks.rb @@ -9,12 +9,10 @@ def read(hook, length) end def write(hook, buffer, length) - data = buffer[0 .. length] - # Handle encoding conversion if the IO has a different encoding - if @io.respond_to?(:external_encoding) && @io.external_encoding - data = data.encode(@io.external_encoding, invalid: :replace, undef: :replace) - end - @io.write(data) + # GPGME output can be arbitrary binary data (for example, an OpenPGP + # message). Do not transcode it to the IO's encoding: conversion can + # replace invalid byte sequences and corrupt the output. + @io.write(buffer.byteslice(0, length)) end def seek(hook, offset, whence) diff --git a/test/io_callbacks_test.rb b/test/io_callbacks_test.rb index db9cafb..d089224 100644 --- a/test/io_callbacks_test.rb +++ b/test/io_callbacks_test.rb @@ -1,6 +1,7 @@ # -*- encoding: utf-8 -*- require 'test_helper' require 'stringio' +require 'tempfile' describe GPGME::IOCallbacks do describe "encoding handling" do @@ -43,21 +44,16 @@ assert_equal "Hello World", io.read end - it "replaces invalid characters when converting encodings" do + it "preserves invalid UTF-8 bytes when writing to a binary IO" do io = StringIO.new - io.set_encoding(Encoding::UTF_8) + io.set_encoding(Encoding::ASCII_8BIT) callbacks = GPGME::IOCallbacks.new(io) - # Invalid UTF-8 sequence in ASCII-8BIT string - invalid_data = "Hello\xC3\x28World".b + invalid_data = "Hello\xC3\x28World".dup.force_encoding(Encoding::UTF_8) - # Should not raise, should replace invalid chars callbacks.write(nil, invalid_data, invalid_data.bytesize) io.rewind - result = io.read - # The invalid sequence should be replaced - refute_nil result - assert result.valid_encoding? + assert_equal invalid_data.bytes, io.read.bytes end it "reads data from IO" do @@ -133,6 +129,29 @@ result.force_encoding(Encoding::UTF_8) assert_equal utf8_text, result end + + it "preserves encrypted output when default internal encoding is UTF-8" do + original_internal = Encoding.default_internal + output = Tempfile.new('gpgme-ciphertext') + output.binmode + plaintext = 'encryption output must remain binary-safe' + + begin + Encoding.default_internal = Encoding::UTF_8 + + crypto = GPGME::Crypto.new(always_trust: true) + crypto.encrypt(plaintext, recipients: KEYS.first[:sha], output: output) + output.close + + ciphertext = File.binread(output.path) + refute_empty ciphertext + assert_equal 0x80, ciphertext.getbyte(0) & 0x80 + ensure + output.close unless output.closed? + output.unlink + Encoding.default_internal = original_internal + end + end end describe "default internal encoding support" do