Skip to content
Open
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
10 changes: 4 additions & 6 deletions lib/gpgme/io_callbacks.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
37 changes: 28 additions & 9 deletions test/io_callbacks_test.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# -*- encoding: utf-8 -*-
require 'test_helper'
require 'stringio'
require 'tempfile'

describe GPGME::IOCallbacks do
describe "encoding handling" do
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading