Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package rubygem-gpgme for openSUSE:Factory 
checked in at 2026-10-30 18:18:09
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/rubygem-gpgme (Old)
 and      /work/SRC/openSUSE:Factory/.rubygem-gpgme.new.1255 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "rubygem-gpgme"

Fri Oct 30 18:18:09 2026 rev:16 rq:1393867 version:2.0.26

Changes:
--------
--- /work/SRC/openSUSE:Factory/rubygem-gpgme/rubygem-gpgme.changes      
2025-07-30 11:45:48.463792580 +0200
+++ /work/SRC/openSUSE:Factory/.rubygem-gpgme.new.1255/rubygem-gpgme.changes    
2026-10-30 18:23:55.711474146 +0100
@@ -1,0 +2,18 @@
+Sat Oct 17 16:05:45 UTC 2026 - Andrea Manzini <[email protected]>
+
+- Update to version 2.0.26:
+  * Replace deprecated Data_Wrap_Struct / Data_Get_Struct with
+    TypedData_Wrap_Struct / TypedData_Get_Struct for Ruby 3.x+
+    compatibility
+  * Use gpgme_op_interact instead of deprecated gpgme_op_edit and
+    gpgme_op_card_edit on GPGME 2.0.0+
+  * Add thread-safe serialization via GPGME.synchronize (enabled by
+    default) to prevent concurrent gpg-agent socket errors
+  * Expose additional GPGME_ENCRYPT_* flag constants
+  * Reuse recipient Key objects when resolving signers in
+    GPGME::Crypto
+  * Respect Encoding.default_internal and IO external encoding in
+    write callbacks
+- Spec file cleaned up
+
+-------------------------------------------------------------------

Old:
----
  gpgme-2.0.25.gem

New:
----
  gpgme-2.0.26.gem

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ rubygem-gpgme.spec ++++++
--- /var/tmp/diff_new_pack.ltqRoJ/_old  2026-10-30 18:23:57.233537862 +0100
+++ /var/tmp/diff_new_pack.ltqRoJ/_new  2026-10-30 18:23:57.236537988 +0100
@@ -1,7 +1,7 @@
 #
 # spec file for package rubygem-gpgme
 #
-# Copyright (c) 2024 SUSE LLC
+# Copyright (c) 2026 SUSE LLC and contributors
 # Copyright (c) 2025 Andreas Stieger <[email protected]>
 #
 # All modifications and additions to the file contributed by third parties
@@ -17,32 +17,31 @@
 #
 
 
+%define mod_name gpgme
+%define mod_full_name %{mod_name}-%{version}
 #
 # This file was generated with a gem2rpm.yml and not just plain gem2rpm.
 # All sections marked as MANUAL, license headers, summaries and descriptions
 # can be maintained in that file. Please consult this file before editing any
 # of those fields
 #
-
 Name:           rubygem-gpgme
-Version:        2.0.25
+Version:        2.0.26
 Release:        0
-%define mod_name gpgme
-%define mod_full_name %{mod_name}-%{version}
+Summary:        Ruby binding of GPGME
+License:        LGPL-2.1-or-later
+URL:            https://github.com/ueno/ruby-gpgme
+Source:         https://rubygems.org/gems/%{mod_full_name}.gem
+Source1:        rubygem-gpgme-rpmlintrc
+Source2:        gem2rpm.yml
+BuildRequires:  %{rubydevel}
+BuildRequires:  %{rubygem gem2rpm}
+BuildRequires:  ruby-macros >= 5
 # MANUAL
-BuildRequires:  pkgconfig(libassuan) >= 2.0.2
 BuildRequires:  pkgconfig(gpg-error) >= 1.8
 BuildRequires:  pkgconfig(gpgme) >= 1.2.0
+BuildRequires:  pkgconfig(libassuan) >= 2.0.2
 # /MANUAL
-BuildRequires:  ruby-macros >= 5
-BuildRequires:  %{rubydevel}
-BuildRequires:  %{rubygem gem2rpm}
-URL:            http://github.com/ueno/ruby-gpgme
-Source:         https://rubygems.org/gems/%{mod_full_name}.gem
-Source1:        rubygem-gpgme-rpmlintrc
-Source2:        gem2rpm.yml
-Summary:        Ruby binding of GPGME
-License:        LGPL-2.1-or-later
 
 %description
 Ruby-GPGME is a Ruby language binding of GPGME (GnuPG
@@ -60,7 +59,7 @@
 # /MANUAL
 %gem_install \
   -f
-%gem_cleanup
+%{gem_cleanup}
 
 %gem_packages
 

++++++ gpgme-2.0.25.gem -> gpgme-2.0.26.gem ++++++
Binary files old/checksums.yaml.gz and new/checksums.yaml.gz differ
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ext/gpgme/extconf.rb new/ext/gpgme/extconf.rb
--- old/ext/gpgme/extconf.rb    2025-07-26 03:04:42.000000000 +0200
+++ new/ext/gpgme/extconf.rb    1980-01-02 01:00:00.000000000 +0100
@@ -225,6 +225,19 @@
 end
 
 have_func('gpgme_op_export_keys')
+have_const('GPGME_ENCRYPT_ALWAYS_TRUST', 'gpgme.h')
+have_const('GPGME_ENCRYPT_NO_ENCRYPT_TO', 'gpgme.h')
+have_const('GPGME_ENCRYPT_PREPARE', 'gpgme.h')
+have_const('GPGME_ENCRYPT_EXPECT_SIGN', 'gpgme.h')
+have_const('GPGME_ENCRYPT_NO_COMPRESS', 'gpgme.h')
+have_const('GPGME_ENCRYPT_SYMMETRIC', 'gpgme.h')
+have_const('GPGME_ENCRYPT_THROW_KEYIDS', 'gpgme.h')
+have_const('GPGME_ENCRYPT_WRAP', 'gpgme.h')
+have_const('GPGME_ENCRYPT_WANT_ADDRESS', 'gpgme.h')
+have_const('GPGME_ENCRYPT_ARCHIVE', 'gpgme.h')
+have_const('GPGME_ENCRYPT_FILE', 'gpgme.h')
+have_const('GPGME_ENCRYPT_ADD_RECP', 'gpgme.h')
+have_const('GPGME_ENCRYPT_CHG_RECP', 'gpgme.h')
 
 create_makefile ('gpgme_n')
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/ext/gpgme/gpgme_n.c new/ext/gpgme/gpgme_n.c
--- old/ext/gpgme/gpgme_n.c     2025-07-26 03:04:42.000000000 +0200
+++ new/ext/gpgme/gpgme_n.c     1980-01-02 01:00:00.000000000 +0100
@@ -88,30 +88,105 @@
 #define RSTRING_LEN(a) RSTRING(a)->len
 #endif
 
+/* TypedData type definitions for Ruby 3.x compatibility */
+static void
+gpgme_data_free(void *ptr)
+{
+  if (ptr)
+    gpgme_data_release((gpgme_data_t)ptr);
+}
+
+static const rb_data_type_t gpgme_data_type = {
+  .wrap_struct_name = "GPGME::Data",
+  .function = {
+    .dmark = NULL,
+    .dfree = gpgme_data_free,
+    .dsize = NULL,
+  },
+  .data = NULL,
+  .flags = 0,
+};
+
+static void
+gpgme_ctx_free(void *ptr)
+{
+  if (ptr)
+    gpgme_release((gpgme_ctx_t)ptr);
+}
+
+static const rb_data_type_t gpgme_ctx_type = {
+  .wrap_struct_name = "GPGME::Ctx",
+  .function = {
+    .dmark = NULL,
+    .dfree = gpgme_ctx_free,
+    .dsize = NULL,
+  },
+  .data = NULL,
+  .flags = 0,
+};
+
+static void
+gpgme_key_free(void *ptr)
+{
+  if (ptr)
+    gpgme_key_unref((gpgme_key_t)ptr);
+}
+
+static const rb_data_type_t gpgme_key_type = {
+  .wrap_struct_name = "GPGME::Key",
+  .function = {
+    .dmark = NULL,
+    .dfree = gpgme_key_free,
+    .dsize = NULL,
+  },
+  .data = NULL,
+  .flags = 0,
+};
+
+#if defined(GPGME_VERSION_NUMBER) && GPGME_VERSION_NUMBER < 0x020000
+static void
+gpgme_trust_item_free(void *ptr)
+{
+  if (ptr)
+    gpgme_trust_item_unref((gpgme_trust_item_t)ptr);
+}
+
+static const rb_data_type_t gpgme_trust_item_type = {
+  .wrap_struct_name = "GPGME::TrustItem",
+  .function = {
+    .dmark = NULL,
+    .dfree = gpgme_trust_item_free,
+    .dsize = NULL,
+  },
+  .data = NULL,
+  .flags = 0,
+};
+#endif
+
 #define WRAP_GPGME_DATA(dh)                                        \
-  Data_Wrap_Struct(cData, 0, gpgme_data_release, dh)
+  TypedData_Wrap_Struct(cData, &gpgme_data_type, dh)
 /* `gpgme_data_t' is typedef'ed as `struct gpgme_data *'. */
 #define UNWRAP_GPGME_DATA(vdh, dh)                                \
-  Data_Get_Struct(vdh, struct gpgme_data, dh);
+  TypedData_Get_Struct(vdh, struct gpgme_data, &gpgme_data_type, dh)
 
 #define WRAP_GPGME_CTX(ctx)                                        \
-  Data_Wrap_Struct(cCtx, 0, gpgme_release, ctx)
+  TypedData_Wrap_Struct(cCtx, &gpgme_ctx_type, ctx)
 /* `gpgme_ctx_t' is typedef'ed as `struct gpgme_context *'. */
 #define UNWRAP_GPGME_CTX(vctx, ctx)                                \
-  Data_Get_Struct(vctx, struct gpgme_context, ctx)
+  TypedData_Get_Struct(vctx, struct gpgme_context, &gpgme_ctx_type, ctx)
 
 #define WRAP_GPGME_KEY(key)                                        \
-  Data_Wrap_Struct(cKey, 0, gpgme_key_unref, key)
+  TypedData_Wrap_Struct(cKey, &gpgme_key_type, key)
 /* `gpgme_key_t' is typedef'ed as `struct _gpgme_key *'. */
 #define UNWRAP_GPGME_KEY(vkey, key)                                \
-  Data_Get_Struct(vkey, struct _gpgme_key, key)
+  TypedData_Get_Struct(vkey, struct _gpgme_key, &gpgme_key_type, key)
 
 #if defined(GPGME_VERSION_NUMBER) && GPGME_VERSION_NUMBER < 0x020000
 #define WRAP_GPGME_TRUST_ITEM(item)                                          \
-  Data_Wrap_Struct(cTrustItem, 0, gpgme_trust_item_unref, item)
+  TypedData_Wrap_Struct(cTrustItem, &gpgme_trust_item_type, item)
 /* `gpgme_trust_item_t' is typedef'ed as `struct _gpgme_trust_item *'. */
 #define UNWRAP_GPGME_TRUST_ITEM(vitem, item)                        \
-  Data_Get_Struct(vitem, struct _gpgme_trust_item, item)
+  TypedData_Get_Struct(vitem, struct _gpgme_trust_item, 
&gpgme_trust_item_type, item)
 #endif
 
 static VALUE cEngineInfo,
@@ -345,11 +420,21 @@
 write_cb (void *handle, const void *buffer, size_t size)
 {
   VALUE vcb = (VALUE)handle, vcbs, vhook_value, vbuffer, vnwrite;
+  rb_encoding *enc;
 
   vcbs = RARRAY_PTR(vcb)[0];
   vhook_value = RARRAY_PTR(vcb)[1];
   vbuffer = rb_str_new (buffer, size);
 
+  /* Associate encoding with the buffer string.
+   * Use default internal encoding if set, otherwise use binary encoding.
+   * This allows the app author to control the encoding via
+   * Encoding.default_internal while maintaining backward compatibility. */
+  enc = rb_default_internal_encoding ();
+  if (!enc)
+    enc = rb_ascii8bit_encoding ();
+  rb_enc_associate (vbuffer, enc);
+
   vnwrite = rb_funcall (vcbs, rb_intern ("write"), 3,
                         vhook_value, vbuffer, LONG2NUM(size));
   return NUM2LONG(vnwrite);
@@ -514,7 +599,7 @@
   if (!ctx)
     rb_raise (rb_eArgError, "released ctx");
   gpgme_release (ctx);
-  DATA_PTR(vctx) = NULL;
+  RTYPEDDATA_DATA(vctx) = NULL;
   return Qnil;
 }
 
@@ -1125,6 +1210,9 @@
       save_gpgme_key_attrs (vkey, key);
       rb_ary_store (rkey, 0, vkey);
     }
+
+  RB_GC_GUARD(vctx);
+
   return LONG2NUM(err);
 }
 
@@ -1540,6 +1628,305 @@
   return LONG2NUM(err);
 }
 
+/*
+ * Key Edit Interface
+ *
+ * GPGME 2.0.0 deprecated gpgme_op_edit, gpgme_op_edit_start, 
gpgme_op_card_edit,
+ * and gpgme_op_card_edit_start in favor of gpgme_op_interact and 
gpgme_op_interact_start.
+ *
+ * The new interact callback has a different signature:
+ *   Old: gpgme_error_t (*)(void *opaque, gpgme_status_code_t status, const 
char *args, int fd)
+ *   New: gpgme_error_t (*)(void *opaque, const char *keyword, const char 
*args, int fd)
+ *
+ * For backward compatibility, we keep the same Ruby interface (gpgme_op_edit, 
etc.)
+ * but internally use gpgme_op_interact for GPGME 2.0.0+, converting keyword 
strings
+ * to status codes in the callback shim.
+ */
+
+#if defined(GPGME_VERSION_NUMBER) && GPGME_VERSION_NUMBER >= 0x020000
+/*
+ * GPGME 2.0.0+: Use gpgme_op_interact internally, but expose the same
+ * gpgme_op_edit interface to Ruby for backward compatibility.
+ *
+ * The shim callback converts keyword strings to status codes.
+ */
+
+/* Macro to define keyword-to-status mapping entries */
+#define STATUS_ENTRY(x) { #x, GPGME_STATUS_##x }
+
+/* Mapping table from keyword strings to status codes */
+static struct {
+  const char *keyword;
+  gpgme_status_code_t status;
+} keyword_to_status[] = {
+  STATUS_ENTRY(EOF),
+  STATUS_ENTRY(ENTER),
+  STATUS_ENTRY(LEAVE),
+  STATUS_ENTRY(ABORT),
+  STATUS_ENTRY(GOODSIG),
+  STATUS_ENTRY(BADSIG),
+  STATUS_ENTRY(ERRSIG),
+  STATUS_ENTRY(BADARMOR),
+  STATUS_ENTRY(RSA_OR_IDEA),
+  STATUS_ENTRY(KEYEXPIRED),
+  STATUS_ENTRY(KEYREVOKED),
+  STATUS_ENTRY(TRUST_UNDEFINED),
+  STATUS_ENTRY(TRUST_NEVER),
+  STATUS_ENTRY(TRUST_MARGINAL),
+  STATUS_ENTRY(TRUST_FULLY),
+  STATUS_ENTRY(TRUST_ULTIMATE),
+  STATUS_ENTRY(SHM_INFO),
+  STATUS_ENTRY(SHM_GET),
+  STATUS_ENTRY(SHM_GET_BOOL),
+  STATUS_ENTRY(SHM_GET_HIDDEN),
+  STATUS_ENTRY(NEED_PASSPHRASE),
+  STATUS_ENTRY(VALIDSIG),
+  STATUS_ENTRY(SIG_ID),
+  STATUS_ENTRY(ENC_TO),
+  STATUS_ENTRY(NODATA),
+  STATUS_ENTRY(BAD_PASSPHRASE),
+  STATUS_ENTRY(NO_PUBKEY),
+  STATUS_ENTRY(NO_SECKEY),
+  STATUS_ENTRY(NEED_PASSPHRASE_SYM),
+  STATUS_ENTRY(DECRYPTION_FAILED),
+  STATUS_ENTRY(DECRYPTION_OKAY),
+  STATUS_ENTRY(MISSING_PASSPHRASE),
+  STATUS_ENTRY(GOOD_PASSPHRASE),
+  STATUS_ENTRY(GOODMDC),
+  STATUS_ENTRY(BADMDC),
+  STATUS_ENTRY(ERRMDC),
+  STATUS_ENTRY(IMPORTED),
+  STATUS_ENTRY(IMPORT_OK),
+  STATUS_ENTRY(IMPORT_PROBLEM),
+  STATUS_ENTRY(IMPORT_RES),
+  STATUS_ENTRY(FILE_START),
+  STATUS_ENTRY(FILE_DONE),
+  STATUS_ENTRY(FILE_ERROR),
+  STATUS_ENTRY(BEGIN_DECRYPTION),
+  STATUS_ENTRY(END_DECRYPTION),
+  STATUS_ENTRY(BEGIN_ENCRYPTION),
+  STATUS_ENTRY(END_ENCRYPTION),
+  STATUS_ENTRY(DELETE_PROBLEM),
+  STATUS_ENTRY(GET_BOOL),
+  STATUS_ENTRY(GET_LINE),
+  STATUS_ENTRY(GET_HIDDEN),
+  STATUS_ENTRY(GOT_IT),
+  STATUS_ENTRY(PROGRESS),
+  STATUS_ENTRY(SIG_CREATED),
+  STATUS_ENTRY(SESSION_KEY),
+  STATUS_ENTRY(NOTATION_NAME),
+  STATUS_ENTRY(NOTATION_DATA),
+  STATUS_ENTRY(POLICY_URL),
+  STATUS_ENTRY(BEGIN_STREAM),
+  STATUS_ENTRY(END_STREAM),
+  STATUS_ENTRY(KEY_CREATED),
+  STATUS_ENTRY(USERID_HINT),
+  STATUS_ENTRY(UNEXPECTED),
+  STATUS_ENTRY(INV_RECP),
+  STATUS_ENTRY(NO_RECP),
+  STATUS_ENTRY(ALREADY_SIGNED),
+  STATUS_ENTRY(SIGEXPIRED),
+  STATUS_ENTRY(EXPSIG),
+  STATUS_ENTRY(EXPKEYSIG),
+  STATUS_ENTRY(TRUNCATED),
+  STATUS_ENTRY(ERROR),
+  STATUS_ENTRY(NEWSIG),
+  STATUS_ENTRY(REVKEYSIG),
+  STATUS_ENTRY(SIG_SUBPACKET),
+  STATUS_ENTRY(NEED_PASSPHRASE_PIN),
+  STATUS_ENTRY(SC_OP_FAILURE),
+  STATUS_ENTRY(SC_OP_SUCCESS),
+  STATUS_ENTRY(CARDCTRL),
+  STATUS_ENTRY(BACKUP_KEY_CREATED),
+  STATUS_ENTRY(PKA_TRUST_BAD),
+  STATUS_ENTRY(PKA_TRUST_GOOD),
+  STATUS_ENTRY(PLAINTEXT),
+  STATUS_ENTRY(INV_SGNR),
+  STATUS_ENTRY(NO_SGNR),
+  STATUS_ENTRY(SUCCESS),
+  STATUS_ENTRY(DECRYPTION_INFO),
+  STATUS_ENTRY(PLAINTEXT_LENGTH),
+  STATUS_ENTRY(MOUNTPOINT),
+  STATUS_ENTRY(PINENTRY_LAUNCHED),
+  STATUS_ENTRY(ATTRIBUTE),
+  STATUS_ENTRY(BEGIN_SIGNING),
+  STATUS_ENTRY(KEY_NOT_CREATED),
+  STATUS_ENTRY(INQUIRE_MAXLEN),
+  STATUS_ENTRY(FAILURE),
+  STATUS_ENTRY(KEY_CONSIDERED),
+  STATUS_ENTRY(TOFU_USER),
+  STATUS_ENTRY(TOFU_STATS),
+  STATUS_ENTRY(TOFU_STATS_LONG),
+  STATUS_ENTRY(NOTATION_FLAGS),
+  STATUS_ENTRY(DECRYPTION_COMPLIANCE_MODE),
+  STATUS_ENTRY(VERIFICATION_COMPLIANCE_MODE),
+  STATUS_ENTRY(CANCELED_BY_USER),
+  { NULL, GPGME_STATUS_EOF }
+};
+
+#undef STATUS_ENTRY
+
+static gpgme_status_code_t
+keyword_to_status_code (const char *keyword)
+{
+  size_t i;
+
+  if (!keyword)
+    return GPGME_STATUS_EOF;
+
+  for (i = 0; keyword_to_status[i].keyword != NULL; i++)
+    {
+      if (strcmp (keyword, keyword_to_status[i].keyword) == 0)
+        return keyword_to_status[i].status;
+    }
+
+  /* Unknown keyword - return EOF as fallback */
+  return GPGME_STATUS_EOF;
+}
+
+/*
+ * Shim callback that converts keyword strings to status codes
+ * for backward compatibility with existing Ruby callbacks.
+ */
+static gpgme_error_t
+edit_cb_shim (void *hook, const char *keyword, const char *args, int fd)
+{
+  VALUE vcb = (VALUE) hook, veditfunc, vhook_value;
+  gpgme_status_code_t status;
+
+  veditfunc = RARRAY_PTR (vcb)[0];
+  vhook_value = RARRAY_PTR (vcb)[1];
+
+  status = keyword_to_status_code (keyword);
+
+  rb_funcall (veditfunc, rb_intern ("call"), 4, vhook_value, INT2FIX (status),
+              args ? rb_str_new2 (args) : rb_str_new2 (""), INT2NUM (fd));
+  return gpgme_err_make (GPG_ERR_SOURCE_USER_1, GPG_ERR_NO_ERROR);
+}
+
+static VALUE
+rb_s_gpgme_op_edit (VALUE dummy, VALUE vctx, VALUE vkey,
+                    VALUE veditfunc, VALUE vhook_value, VALUE vout)
+{
+  gpgme_ctx_t ctx;
+  gpgme_key_t key;
+  gpgme_data_t out = NULL;
+  VALUE vcb;
+  gpgme_error_t err;
+
+  CHECK_KEYLIST_NOT_IN_PROGRESS (vctx);
+
+  UNWRAP_GPGME_CTX(vctx, ctx);
+  if (!ctx)
+    rb_raise (rb_eArgError, "released ctx");
+  UNWRAP_GPGME_KEY(vkey, key);
+  if (!NIL_P(vout))
+    UNWRAP_GPGME_DATA(vout, out);
+
+  vcb = rb_ary_new ();
+  rb_ary_push (vcb, veditfunc);
+  rb_ary_push (vcb, vhook_value);
+  /* Keep a reference to avoid GC. */
+  rb_iv_set (vctx, "@edit_cb", vcb);
+
+  /* Use gpgme_op_interact with flags=0, shim converts keywords to status 
codes */
+  err = gpgme_op_interact (ctx, key, 0, edit_cb_shim, (void *)vcb, out);
+  return LONG2NUM(err);
+}
+
+static VALUE
+rb_s_gpgme_op_edit_start (VALUE dummy, VALUE vctx, VALUE vkey,
+                          VALUE veditfunc, VALUE vhook_value, VALUE vout)
+{
+  gpgme_ctx_t ctx;
+  gpgme_key_t key;
+  gpgme_data_t out = NULL;
+  VALUE vcb;
+  gpgme_error_t err;
+
+  CHECK_KEYLIST_NOT_IN_PROGRESS(vctx);
+
+  UNWRAP_GPGME_CTX(vctx, ctx);
+  if (!ctx)
+    rb_raise (rb_eArgError, "released ctx");
+  UNWRAP_GPGME_KEY(vkey, key);
+  if (!NIL_P(vout))
+    UNWRAP_GPGME_DATA(vout, out);
+
+  vcb = rb_ary_new ();
+  rb_ary_push (vcb, veditfunc);
+  rb_ary_push (vcb, vhook_value);
+  /* Keep a reference to avoid GC. */
+  rb_iv_set (vctx, "@edit_cb", vcb);
+
+  /* Use gpgme_op_interact_start with flags=0, shim converts keywords to 
status codes */
+  err = gpgme_op_interact_start (ctx, key, 0, edit_cb_shim, (void *)vcb, out);
+  return LONG2NUM(err);
+}
+
+static VALUE
+rb_s_gpgme_op_card_edit (VALUE dummy, VALUE vctx, VALUE vkey,
+                    VALUE veditfunc, VALUE vhook_value, VALUE vout)
+{
+  gpgme_ctx_t ctx;
+  gpgme_key_t key;
+  gpgme_data_t out = NULL;
+  VALUE vcb;
+  gpgme_error_t err;
+
+  CHECK_KEYLIST_NOT_IN_PROGRESS(vctx);
+
+  UNWRAP_GPGME_CTX(vctx, ctx);
+  if (!ctx)
+    rb_raise (rb_eArgError, "released ctx");
+  UNWRAP_GPGME_KEY(vkey, key);
+  if (!NIL_P(vout))
+    UNWRAP_GPGME_DATA(vout, out);
+
+  vcb = rb_ary_new ();
+  rb_ary_push (vcb, veditfunc);
+  rb_ary_push (vcb, vhook_value);
+  /* Keep a reference to avoid GC. */
+  rb_iv_set (vctx, "@card_edit_cb", vcb);
+
+  /* Use gpgme_op_interact with GPGME_INTERACT_CARD flag */
+  err = gpgme_op_interact (ctx, key, GPGME_INTERACT_CARD, edit_cb_shim, (void 
*)vcb, out);
+  return LONG2NUM(err);
+}
+
+static VALUE
+rb_s_gpgme_op_card_edit_start (VALUE dummy, VALUE vctx, VALUE vkey,
+                               VALUE veditfunc, VALUE vhook_value, VALUE vout)
+{
+  gpgme_ctx_t ctx;
+  gpgme_key_t key;
+  gpgme_data_t out = NULL;
+  VALUE vcb;
+  gpgme_error_t err;
+
+  CHECK_KEYLIST_NOT_IN_PROGRESS(vctx);
+
+  UNWRAP_GPGME_CTX(vctx, ctx);
+  if (!ctx)
+    rb_raise (rb_eArgError, "released ctx");
+  UNWRAP_GPGME_KEY(vkey, key);
+  if (!NIL_P(vout))
+    UNWRAP_GPGME_DATA(vout, out);
+
+  vcb = rb_ary_new ();
+  rb_ary_push (vcb, veditfunc);
+  rb_ary_push (vcb, vhook_value);
+  /* Keep a reference to avoid GC. */
+  rb_iv_set (vctx, "@card_edit_cb", vcb);
+
+  /* Use gpgme_op_interact_start with GPGME_INTERACT_CARD flag */
+  err = gpgme_op_interact_start (ctx, key, GPGME_INTERACT_CARD, edit_cb_shim, 
(void *)vcb, out);
+  return LONG2NUM(err);
+}
+
+#else
+/* GPGME < 2.0.0: Use the original gpgme_op_edit functions directly */
+
 static gpgme_error_t
 edit_cb (void *hook, gpgme_status_code_t status, const char *args, int fd)
 {
@@ -1669,6 +2056,7 @@
   err = gpgme_op_card_edit_start (ctx, key, edit_cb, (void *)vcb, out);
   return LONG2NUM(err);
 }
+#endif /* GPGME_VERSION_NUMBER >= 0x020000 */
 
 #if defined(GPGME_VERSION_NUMBER) && GPGME_VERSION_NUMBER < 0x020000
 static VALUE
@@ -1752,6 +2140,11 @@
   UNWRAP_GPGME_DATA(vplain, plain);
 
   err = gpgme_op_decrypt (ctx, cipher, plain);
+
+  RB_GC_GUARD(vctx);
+  RB_GC_GUARD(vcipher);
+  RB_GC_GUARD(vplain);
+
   return LONG2NUM(err);
 }
 
@@ -1831,6 +2224,12 @@
     UNWRAP_GPGME_DATA(vplain, plain);
 
   err = gpgme_op_verify (ctx, sig, signed_text, plain);
+
+  RB_GC_GUARD(vctx);
+  RB_GC_GUARD(vsig);
+  RB_GC_GUARD(vsigned_text);
+  RB_GC_GUARD(vplain);
+
   return LONG2NUM(err);
 }
 
@@ -1935,6 +2334,11 @@
   UNWRAP_GPGME_DATA(vplain, plain);
 
   err = gpgme_op_decrypt_verify (ctx, cipher, plain);
+
+  RB_GC_GUARD(vctx);
+  RB_GC_GUARD(vcipher);
+  RB_GC_GUARD(vplain);
+
   return LONG2NUM(err);
 }
 
@@ -2019,6 +2423,11 @@
   UNWRAP_GPGME_DATA(vsig, sig);
 
   err = gpgme_op_sign (ctx, plain, sig, NUM2INT(vmode));
+
+  RB_GC_GUARD(vctx);
+  RB_GC_GUARD(vplain);
+  RB_GC_GUARD(vsig);
+
   return LONG2NUM(err);
 }
 
@@ -2128,6 +2537,12 @@
   err = gpgme_op_encrypt (ctx, recp, NUM2INT(vflags), plain, cipher);
   if (recp)
     xfree (recp);
+
+  RB_GC_GUARD(vctx);
+  RB_GC_GUARD(vrecp);
+  RB_GC_GUARD(vplain);
+  RB_GC_GUARD(vcipher);
+
   return LONG2NUM(err);
 }
 
@@ -2227,6 +2642,12 @@
   err = gpgme_op_encrypt_sign (ctx, recp, NUM2INT(vflags), plain, cipher);
   if (recp)
     xfree (recp);
+
+  RB_GC_GUARD(vctx);
+  RB_GC_GUARD(vrecp);
+  RB_GC_GUARD(vplain);
+  RB_GC_GUARD(vcipher);
+
   return LONG2NUM(err);
 }
 
@@ -2395,7 +2816,7 @@
   UNWRAP_GPGME_CTX(vctx, ctx);
   if (!ctx)
     rb_raise (rb_eArgError, "released ctx");
-  
+
   size = NUM2SIZET(vsize);
   buffer = ALLOC_N(char, size);
 
@@ -2648,6 +3069,11 @@
                              rb_s_gpgme_op_delete_ext, 3);
   rb_define_module_function (mGPGME, "gpgme_op_delete_start",
                              rb_s_gpgme_op_delete_start, 3);
+
+  /* Key Edit Interface
+   * For GPGME 2.0.0+, these functions use gpgme_op_interact internally
+   * with a shim to maintain backward compatibility with existing callbacks.
+   */
   rb_define_module_function (mGPGME, "gpgme_op_edit",
                              rb_s_gpgme_op_edit, 5);
   rb_define_module_function (mGPGME, "gpgme_op_edit_start",
@@ -3218,11 +3644,58 @@
   /* The available flags for gpgme_op_encrypt.  */
   rb_define_const (mGPGME, "GPGME_ENCRYPT_ALWAYS_TRUST",
                    INT2FIX(GPGME_ENCRYPT_ALWAYS_TRUST));
-  /* This flag was added in 1.2.0. */
-#ifdef GPGME_ENCRYPT_NO_ENCRYPT_TO
+#ifdef HAVE_CONST_GPGME_ENCRYPT_NO_ENCRYPT_TO
   rb_define_const (mGPGME, "GPGME_ENCRYPT_NO_ENCRYPT_TO",
                    INT2FIX(GPGME_ENCRYPT_NO_ENCRYPT_TO));
 #endif
+#ifdef HAVE_CONST_GPGME_ENCRYPT_PREPARE
+  rb_define_const (mGPGME, "GPGME_ENCRYPT_PREPARE",
+                   INT2FIX(GPGME_ENCRYPT_PREPARE));
+#endif
+#ifdef HAVE_CONST_GPGME_ENCRYPT_EXPECT_SIGN
+  rb_define_const (mGPGME, "GPGME_ENCRYPT_EXPECT_SIGN",
+                   INT2FIX(GPGME_ENCRYPT_EXPECT_SIGN));
+#endif
+#ifdef HAVE_CONST_GPGME_ENCRYPT_NO_COMPRESS
+  rb_define_const (mGPGME, "GPGME_ENCRYPT_NO_COMPRESS",
+                   INT2FIX(GPGME_ENCRYPT_NO_COMPRESS));
+#endif
+#ifdef HAVE_CONST_GPGME_ENCRYPT_UNSIGNED_INTEGRITY_CHECK
+  rb_define_const (mGPGME, "GPGME_ENCRYPT_UNSIGNED_INTEGRITY_CHECK",
+                   INT2FIX(GPGME_ENCRYPT_UNSIGNED_INTEGRITY_CHECK));
+#endif
+#ifdef HAVE_CONST_GPGME_ENCRYPT_SYMMETRIC
+  rb_define_const (mGPGME, "GPGME_ENCRYPT_SYMMETRIC",
+                   INT2FIX(GPGME_ENCRYPT_SYMMETRIC));
+#endif
+#ifdef HAVE_CONST_GPGME_ENCRYPT_THROW_KEYIDS
+  rb_define_const (mGPGME, "GPGME_ENCRYPT_THROW_KEYIDS",
+                   INT2FIX(GPGME_ENCRYPT_THROW_KEYIDS));
+#endif
+#ifdef HAVE_CONST_GPGME_ENCRYPT_WRAP
+  rb_define_const (mGPGME, "GPGME_ENCRYPT_WRAP",
+                   INT2FIX(GPGME_ENCRYPT_WRAP));
+#endif
+#ifdef HAVE_CONST_GPGME_ENCRYPT_WANT_ADDRESS
+  rb_define_const (mGPGME, "GPGME_ENCRYPT_WANT_ADDRESS",
+                   INT2FIX(GPGME_ENCRYPT_WANT_ADDRESS));
+#endif
+#ifdef HAVE_CONST_GPGME_ENCRYPT_ARCHIVE
+  rb_define_const (mGPGME, "GPGME_ENCRYPT_ARCHIVE",
+                   INT2FIX(GPGME_ENCRYPT_ARCHIVE));
+#endif
+#ifdef HAVE_CONST_GPGME_ENCRYPT_FILE
+  rb_define_const (mGPGME, "GPGME_ENCRYPT_FILE",
+                   INT2FIX(GPGME_ENCRYPT_FILE));
+#endif
+#ifdef HAVE_CONST_GPGME_ENCRYPT_ADD_RECP
+  rb_define_const (mGPGME, "GPGME_ENCRYPT_ADD_RECP",
+                   INT2FIX(GPGME_ENCRYPT_ADD_RECP));
+#endif
+#ifdef HAVE_CONST_GPGME_ENCRYPT_CHG_RECP
+  rb_define_const (mGPGME, "GPGME_ENCRYPT_CHG_RECP",
+                   INT2FIX(GPGME_ENCRYPT_CHG_RECP));
+#endif
 
   /* Random number generation mode flags added in 2.0.0 */
 #if defined(GPGME_VERSION_NUMBER) && GPGME_VERSION_NUMBER >= 0x020000
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lib/gpgme/constants.rb new/lib/gpgme/constants.rb
--- old/lib/gpgme/constants.rb  2025-07-26 03:04:42.000000000 +0200
+++ new/lib/gpgme/constants.rb  1980-01-02 01:00:00.000000000 +0100
@@ -101,6 +101,24 @@
   if defined?(GPGME_ENCRYPT_NO_ENCRYPT_TO)
     ENCRYPT_NO_ENCRYPT_TO = GPGME_ENCRYPT_NO_ENCRYPT_TO
   end
+  if defined?(GPGME_ENCRYPT_PREPARE)
+    ENCRYPT_PREPARE = GPGME_ENCRYPT_PREPARE
+  end
+  if defined?(GPGME_ENCRYPT_EXPECT_SIGN)
+    ENCRYPT_EXPECT_SIGN = GPGME_ENCRYPT_EXPECT_SIGN
+  end
+  if defined?(GPGME_ENCRYPT_NO_COMPRESS)
+    ENCRYPT_NO_COMPRESS = GPGME_ENCRYPT_NO_COMPRESS
+  end
+  if defined?(GPGME_ENCRYPT_UNSIGNED_INTEGRITY_CHECK)
+    ENCRYPT_UNSIGNED_INTEGRITY_CHECK = GPGME_ENCRYPT_UNSIGNED_INTEGRITY_CHECK
+  end
+  if defined?(GPGME_ENCRYPT_SYMMETRIC)
+    ENCRYPT_SYMMETRIC = GPGME_ENCRYPT_SYMMETRIC
+  end
+  if defined?(GPGME_ENCRYPT_THROW_KEYIDS)
+    ENCRYPT_THROW_KEYIDS = GPGME_ENCRYPT_THROW_KEYIDS
+  end
   IMPORT_NEW = GPGME_IMPORT_NEW
   IMPORT_SECRET = GPGME_IMPORT_SECRET
   IMPORT_SIG = GPGME_IMPORT_SIG
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lib/gpgme/crypto.rb new/lib/gpgme/crypto.rb
--- old/lib/gpgme/crypto.rb     2025-07-26 03:04:42.000000000 +0200
+++ new/lib/gpgme/crypto.rb     1980-01-02 01:00:00.000000000 +0100
@@ -91,7 +91,9 @@
         begin
           if options[:sign]
             if options[:signers]
-              signers = Key.find(:public, options[:signers], :sign)
+              # Optimization: reuse recipient Key objects if signers match
+              # to avoid redundant key lookups
+              signers = resolve_keys_for_signing(options[:signers], keys)
               ctx.add_signer(*signers)
             end
             ctx.encrypt_sign(keys, plain_data, cipher_data, flags)
@@ -353,5 +355,58 @@
       end
     end
 
+    private
+
+    # Resolves keys for signing, reusing already-fetched recipient keys when 
possible
+    # to avoid redundant key lookups which can be slow.
+    #
+    # @param signers_input [Array, String, Key] The signer identifiers
+    # @param recipient_keys [Array<Key>, nil] Already-fetched recipient keys 
to check for reuse
+    # @return [Array<Key>] Keys suitable for signing
+    def resolve_keys_for_signing(signers_input, recipient_keys)
+      signers_input = [signers_input].flatten
+      recipient_keys ||= []
+
+      # Build a lookup hash of recipient keys by various identifiers
+      recipient_lookup = {}
+      recipient_keys.each do |key|
+        next unless key.is_a?(Key)
+        # Index by fingerprint, short key ID, and email
+        recipient_lookup[key.fingerprint] = key if key.fingerprint
+        recipient_lookup[key.fingerprint[-16..]] = key if key.fingerprint && 
key.fingerprint.length >= 16
+        recipient_lookup[key.fingerprint[-8..]] = key if key.fingerprint && 
key.fingerprint.length >= 8
+        if key.uids && !key.uids.empty?
+          key.uids.each do |uid|
+            recipient_lookup[uid.email] = key if uid.email && !uid.email.empty?
+          end
+        end
+      end
+
+      result = []
+      needs_lookup = []
+
+      signers_input.each do |signer|
+        case signer
+        when Key
+          # Already a key object, use directly if it can sign
+          result << signer if signer.usable_for?([:sign])
+        when String
+          # Check if we already have this key from recipients
+          if (existing = recipient_lookup[signer]) && 
existing.usable_for?([:sign])
+            result << existing
+          else
+            needs_lookup << signer
+          end
+        end
+      end
+
+      # Only do a Key.find for signers we couldn't resolve from recipients
+      unless needs_lookup.empty?
+        result += Key.find(:public, needs_lookup, :sign)
+      end
+
+      result
+    end
+
   end # module Crypto
 end # module GPGME
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lib/gpgme/ctx.rb new/lib/gpgme/ctx.rb
--- old/lib/gpgme/ctx.rb        2025-07-26 03:04:42.000000000 +0200
+++ new/lib/gpgme/ctx.rb        1980-01-02 01:00:00.000000000 +0100
@@ -76,10 +76,12 @@
       end
 
       if block_given?
-        begin
-          yield ctx
-        ensure
-          GPGME::gpgme_release(ctx)
+        GPGME.synchronize do
+          begin
+            yield ctx
+          ensure
+            GPGME::gpgme_release(ctx)
+          end
         end
       else
         ctx
@@ -464,6 +466,9 @@
     alias delete delete_key
 
     # Edit attributes of the key in the local key ring.
+    #
+    # The callback receives (hook_value, status, args, fd) where status is
+    # a numeric status code (e.g., GPGME::GPGME_STATUS_GET_BOOL).
     def edit_key(key, editfunc, hook_value = nil, out = Data.new)
       err = GPGME::gpgme_op_edit(self, key, editfunc, hook_value, out)
       exc = GPGME::error_to_exception(err)
@@ -472,6 +477,9 @@
     alias edit edit_key
 
     # Edit attributes of the key on the card.
+    #
+    # The callback receives (hook_value, status, args, fd) where status is
+    # a numeric status code (e.g., GPGME::GPGME_STATUS_GET_BOOL).
     def edit_card_key(key, editfunc, hook_value = nil, out = Data.new)
       err = GPGME::gpgme_op_card_edit(self, key, editfunc, hook_value, out)
       exc = GPGME::error_to_exception(err)
@@ -612,10 +620,21 @@
     private
 
     def self.pass_function(pass, uid_hint, passphrase_info, prev_was_bad, fd)
+      # Write the passphrase directly using IO.for_fd. We set autoclose=false
+      # to prevent Ruby from closing the fd (which belongs to GPGME/gpg-agent).
+      # The IO object is used only within this method scope and not stored,
+      # so we also ensure it isn't prematurely collected by keeping a strong
+      # reference until we're done.
       io = IO.for_fd(fd, 'w')
       io.autoclose = false
-      io.puts pass
-      io.flush
+      begin
+        io.write "#{pass}\n"
+        io.flush
+      rescue => e
+        # If the fd has become invalid (e.g. agent communication error),
+        # re-raise as a more descriptive error.
+        raise e
+      end
     end
 
   end
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lib/gpgme/io_callbacks.rb 
new/lib/gpgme/io_callbacks.rb
--- old/lib/gpgme/io_callbacks.rb       2025-07-26 03:04:42.000000000 +0200
+++ new/lib/gpgme/io_callbacks.rb       1980-01-02 01:00:00.000000000 +0100
@@ -9,7 +9,12 @@
     end
 
     def write(hook, buffer, length)
-      @io.write(buffer[0 .. 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)
     end
 
     def seek(hook, offset, whence)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lib/gpgme/version.rb new/lib/gpgme/version.rb
--- old/lib/gpgme/version.rb    2025-07-26 03:04:42.000000000 +0200
+++ new/lib/gpgme/version.rb    1980-01-02 01:00:00.000000000 +0100
@@ -1,4 +1,4 @@
 module GPGME
   # The version of GPGME ruby binding you are using
-  VERSION = "2.0.25"
+  VERSION = "2.0.26"
 end
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lib/gpgme.rb new/lib/gpgme.rb
--- old/lib/gpgme.rb    2025-07-26 03:04:42.000000000 +0200
+++ new/lib/gpgme.rb    1980-01-02 01:00:00.000000000 +0100
@@ -1,6 +1,8 @@
 require 'gpgme_n'
+require 'monitor'
 
-# TODO without this call one can't GPGME::Ctx.new, find out why
+# This call initializes the GPGME library and must happen before
+# any GPGME operations (e.g. Ctx.new) can succeed.
 GPGME::gpgme_check_version(nil)
 
 require 'gpgme/constants'
@@ -19,8 +21,61 @@
 require 'gpgme/crypto'
 
 module GPGME
+
+  # Mutex for serializing GPGME operations when thread safety is enabled.
+  # While the underlying GPGME C library supports separate contexts in
+  # separate threads, the communication with gpg-agent over Unix domain
+  # sockets can produce "Bad file descriptor" errors under heavy concurrent
+  # load. Thread-safe mode is enabled by default and can be disabled
+  # if not needed (e.g. single-threaded applications).
+  #
+  # A Monitor is used instead of a Mutex because GPGME operations are
+  # reentrant — e.g. Crypto#sign calls Ctx.new, and within that block,
+  # Key.find calls Ctx.new again.
+  #
+  # @example Disable thread-safe mode for single-threaded apps
+  #   GPGME.thread_safe = false
+  #
+  @thread_safe_mutex = Monitor.new
+  @thread_safe = true
+
   class << self
 
+    # Enable or disable thread-safe mode. Enabled by default. When
+    # enabled, all high-level GPGME operations (encrypt, decrypt, sign,
+    # verify, key listing, etc.) are serialized through a global monitor
+    # to prevent concurrent access to gpg-agent from causing "Bad file
+    # descriptor" errors. Disable for single-threaded apps if the
+    # synchronization overhead is undesirable.
+    #
+    # @param [Boolean] value false to disable thread-safe mode
+    attr_writer :thread_safe
+
+    # Returns true if thread-safe mode is enabled.
+    def thread_safe?
+      @thread_safe
+    end
+
+    # The mutex used for thread-safe serialization. Can be used directly
+    # if you need finer-grained control over locking.
+    #
+    # @example manual locking
+    #   GPGME.synchronize do
+    #     # multiple GPGME operations atomically
+    #   end
+    attr_reader :thread_safe_mutex
+
+    # Execute a block with the GPGME mutex held if thread-safe mode is
+    # enabled. If thread-safe mode is disabled, the block is executed
+    # directly without locking.
+    def synchronize(&block)
+      if @thread_safe
+        @thread_safe_mutex.synchronize(&block)
+      else
+        yield
+      end
+    end
+
     # From the c extension
     alias pubkey_algo_name gpgme_pubkey_algo_name
     alias hash_algo_name gpgme_hash_algo_name
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/metadata new/metadata
--- old/metadata        2025-07-26 03:04:42.000000000 +0200
+++ new/metadata        1980-01-02 01:00:00.000000000 +0100
@@ -1,12 +1,11 @@
 --- !ruby/object:Gem::Specification
 name: gpgme
 version: !ruby/object:Gem::Version
-  version: 2.0.25
+  version: 2.0.26
 platform: ruby
 authors:
 - Daiki Ueno
 - Albert Llop
-autorequire:
 bindir: bin
 cert_chain: []
 date: 2025-07-26 00:00:00.000000000 Z
@@ -137,10 +136,12 @@
 - test/crypto_test.rb
 - test/ctx_test.rb
 - test/data_test.rb
+- test/encryption_flags_test.rb
 - test/files/testkey_pub.gpg
 - test/files/testkey_pub_invalid.gpg
 - test/files/testkey_sec.gpg
 - test/gpgme_test.rb
+- test/io_callbacks_test.rb
 - test/key_test.rb
 - test/pinentry
 - test/signature_test.rb
@@ -151,7 +152,6 @@
 licenses:
 - LGPL-2.1+
 metadata: {}
-post_install_message:
 rdoc_options: []
 require_paths:
 - lib
@@ -167,8 +167,7 @@
     - !ruby/object:Gem::Version
       version: '0'
 requirements: []
-rubygems_version: 3.5.22
-signing_key:
+rubygems_version: 3.7.2
 specification_version: 4
 summary: Ruby binding of GPGME.
 test_files: []
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/test/ctx_test.rb new/test/ctx_test.rb
--- old/test/ctx_test.rb        2025-07-26 03:04:42.000000000 +0200
+++ new/test/ctx_test.rb        1980-01-02 01:00:00.000000000 +0100
@@ -70,9 +70,6 @@
     end
 
     it "should not segfault" do
-      cipher = GPGME::Data.new(KEY_1_ENCRYPTED)
-      ouput = GPGME::Data.new
-      
       GPGME::Ctx.new do |ctx|
         assert_raises ArgumentError do
           ctx.decrypt_result
@@ -214,7 +211,7 @@
 
     it "doesn't allow just any value" do
       assert_raises GPGME::Error::InvalidValue do
-        ctx = GPGME::Ctx.new(:protocol => -200)
+        GPGME::Ctx.new(:protocol => -200)
       end
     end
   end
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/test/encryption_flags_test.rb 
new/test/encryption_flags_test.rb
--- old/test/encryption_flags_test.rb   1970-01-01 01:00:00.000000000 +0100
+++ new/test/encryption_flags_test.rb   1980-01-02 01:00:00.000000000 +0100
@@ -0,0 +1,65 @@
+# -*- encoding: utf-8 -*-
+require 'test_helper'
+
+describe 'GPGME Encryption Flags' do
+  it 'should expose ENCRYPT_ALWAYS_TRUST' do
+    assert_equal 1, GPGME::ENCRYPT_ALWAYS_TRUST
+  end
+
+  it 'should expose ENCRYPT_NO_ENCRYPT_TO if available' do
+    if defined?(GPGME::ENCRYPT_NO_ENCRYPT_TO)
+      assert GPGME::ENCRYPT_NO_ENCRYPT_TO.is_a?(Integer)
+    end
+  end
+
+  it 'should expose ENCRYPT_PREPARE if available' do
+    if defined?(GPGME::ENCRYPT_PREPARE)
+      assert GPGME::ENCRYPT_PREPARE.is_a?(Integer)
+    end
+  end
+
+  it 'should expose ENCRYPT_EXPECT_SIGN if available' do
+    if defined?(GPGME::ENCRYPT_EXPECT_SIGN)
+      assert GPGME::ENCRYPT_EXPECT_SIGN.is_a?(Integer)
+    end
+  end
+
+  it 'should expose ENCRYPT_NO_COMPRESS if available' do
+    if defined?(GPGME::ENCRYPT_NO_COMPRESS)
+      assert GPGME::ENCRYPT_NO_COMPRESS.is_a?(Integer)
+    end
+  end
+
+  it 'should expose ENCRYPT_UNSIGNED_INTEGRITY_CHECK if available' do
+    if defined?(GPGME::ENCRYPT_UNSIGNED_INTEGRITY_CHECK)
+      assert GPGME::ENCRYPT_UNSIGNED_INTEGRITY_CHECK.is_a?(Integer)
+    end
+  end
+
+  it 'should expose ENCRYPT_SYMMETRIC if available' do
+    if defined?(GPGME::ENCRYPT_SYMMETRIC)
+      assert GPGME::ENCRYPT_SYMMETRIC.is_a?(Integer)
+    end
+  end
+
+  it 'should expose ENCRYPT_THROW_KEYIDS if available' do
+    if defined?(GPGME::ENCRYPT_THROW_KEYIDS)
+      assert GPGME::ENCRYPT_THROW_KEYIDS.is_a?(Integer)
+    end
+  end
+
+  it 'should use different flag values for different flags' do
+    flags = []
+    flags << GPGME::ENCRYPT_ALWAYS_TRUST
+    flags << GPGME::ENCRYPT_NO_ENCRYPT_TO if 
defined?(GPGME::ENCRYPT_NO_ENCRYPT_TO)
+    flags << GPGME::ENCRYPT_PREPARE if defined?(GPGME::ENCRYPT_PREPARE)
+    flags << GPGME::ENCRYPT_EXPECT_SIGN if defined?(GPGME::ENCRYPT_EXPECT_SIGN)
+    flags << GPGME::ENCRYPT_NO_COMPRESS if defined?(GPGME::ENCRYPT_NO_COMPRESS)
+    flags << GPGME::ENCRYPT_UNSIGNED_INTEGRITY_CHECK if 
defined?(GPGME::ENCRYPT_UNSIGNED_INTEGRITY_CHECK)
+    flags << GPGME::ENCRYPT_SYMMETRIC if defined?(GPGME::ENCRYPT_SYMMETRIC)
+    flags << GPGME::ENCRYPT_THROW_KEYIDS if 
defined?(GPGME::ENCRYPT_THROW_KEYIDS)
+
+    # All flags should be unique
+    assert_equal flags.length, flags.uniq.length
+  end
+end
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/test/io_callbacks_test.rb 
new/test/io_callbacks_test.rb
--- old/test/io_callbacks_test.rb       1970-01-01 01:00:00.000000000 +0100
+++ new/test/io_callbacks_test.rb       1980-01-02 01:00:00.000000000 +0100
@@ -0,0 +1,169 @@
+# -*- encoding: utf-8 -*-
+require 'test_helper'
+require 'stringio'
+
+describe GPGME::IOCallbacks do
+  describe "encoding handling" do
+    it "writes binary data to binary IO without error" do
+      io = StringIO.new
+      io.set_encoding(Encoding::ASCII_8BIT)
+      callbacks = GPGME::IOCallbacks.new(io)
+
+      # Binary data with bytes that aren't valid UTF-8
+      binary_data = "\xC3\x28".b # Invalid UTF-8 sequence
+
+      callbacks.write(nil, binary_data, binary_data.bytesize)
+      io.rewind
+      assert_equal binary_data, io.read
+    end
+
+    it "writes UTF-8 data to UTF-8 IO without error" do
+      io = StringIO.new
+      io.set_encoding(Encoding::UTF_8)
+      callbacks = GPGME::IOCallbacks.new(io)
+
+      utf8_data = "Héllo Wörld! 日本語"
+
+      callbacks.write(nil, utf8_data.encode(Encoding::UTF_8), 
utf8_data.bytesize)
+      io.rewind
+      assert_equal utf8_data, io.read
+    end
+
+    it "handles encoding conversion when IO has different encoding" do
+      io = StringIO.new
+      io.set_encoding(Encoding::UTF_8)
+      callbacks = GPGME::IOCallbacks.new(io)
+
+      # ASCII-8BIT string with valid UTF-8 bytes
+      data = "Hello World".b
+
+      # Should not raise Encoding::UndefinedConversionError
+      callbacks.write(nil, data, data.bytesize)
+      io.rewind
+      assert_equal "Hello World", io.read
+    end
+
+    it "replaces invalid characters when converting encodings" do
+      io = StringIO.new
+      io.set_encoding(Encoding::UTF_8)
+      callbacks = GPGME::IOCallbacks.new(io)
+
+      # Invalid UTF-8 sequence in ASCII-8BIT string
+      invalid_data = "Hello\xC3\x28World".b
+
+      # 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?
+    end
+
+    it "reads data from IO" do
+      io = StringIO.new("test data")
+      callbacks = GPGME::IOCallbacks.new(io)
+
+      result = callbacks.read(nil, 9)
+      assert_equal "test data", result
+    end
+
+    it "seeks in IO" do
+      io = StringIO.new("test data")
+      callbacks = GPGME::IOCallbacks.new(io)
+
+      callbacks.read(nil, 4) # read "test"
+      pos = callbacks.seek(nil, 0, IO::SEEK_SET)
+      assert_equal 0, pos
+
+      result = callbacks.read(nil, 4)
+      assert_equal "test", result
+    end
+
+    it "returns current position for seek with offset 0 and SEEK_CUR" do
+      io = StringIO.new("test data")
+      callbacks = GPGME::IOCallbacks.new(io)
+
+      callbacks.read(nil, 5) # read "test "
+      pos = callbacks.seek(nil, 0, IO::SEEK_CUR)
+      assert_equal 5, pos
+    end
+  end
+
+  describe "integration with GPGME signing" do
+    before do
+      skip unless ensure_keys GPGME::PROTOCOL_OpenPGP
+    end
+
+    it "clearsigns UTF-8 data without encoding errors" do
+      utf8_text = "Héllo Wörld! Ünïcödé tëxt 日本語"
+
+      crypto = GPGME::Crypto.new
+      output = StringIO.new
+      output.set_encoding(Encoding::UTF_8)
+
+      # This should not raise Encoding::UndefinedConversionError
+      crypto.sign(utf8_text, mode: GPGME::SIG_MODE_CLEAR, output: output)
+
+      output.rewind
+      result = output.read
+      refute_empty result
+      assert result.include?("BEGIN PGP SIGNED MESSAGE")
+    end
+
+    it "signs UTF-8 data and outputs to default buffer without errors" do
+      utf8_text = "Ünïcödé tëxt: äöü ÄÖÜ ß"
+
+      crypto = GPGME::Crypto.new
+      signed = crypto.sign(utf8_text)
+
+      result = signed.read
+      refute_empty result
+    end
+
+    it "encrypts and decrypts UTF-8 data correctly" do
+      utf8_text = "Sëcrét mëssägé with spëcïäl chäräctërs: 日本語"
+
+      crypto = GPGME::Crypto.new(always_trust: true)
+      encrypted = crypto.encrypt(utf8_text, recipients: KEYS.first[:sha])
+      decrypted = crypto.decrypt(encrypted)
+
+      result = decrypted.read
+      # Force UTF-8 encoding since GPGME returns binary data
+      result.force_encoding(Encoding::UTF_8)
+      assert_equal utf8_text, result
+    end
+  end
+
+  describe "default internal encoding support" do
+    it "respects Encoding.default_internal when set" do
+      # Save original setting
+      original_internal = Encoding.default_internal
+      original_verbose = $VERBOSE
+
+      begin
+        # Suppress warning about setting Encoding.default_internal
+        $VERBOSE = nil
+        Encoding.default_internal = Encoding::UTF_8
+        $VERBOSE = original_verbose
+
+        io = StringIO.new
+        io.set_encoding(Encoding::UTF_8)
+        callbacks = GPGME::IOCallbacks.new(io)
+
+        # Valid UTF-8 data
+        utf8_data = "Tëst dätä"
+        callbacks.write(nil, utf8_data, utf8_data.bytesize)
+
+        io.rewind
+        result = io.read
+        assert_equal utf8_data, result
+      ensure
+        # Restore original settings
+        $VERBOSE = nil
+        Encoding.default_internal = original_internal
+        $VERBOSE = original_verbose
+      end
+    end
+  end
+end

Reply via email to