Submitted By:            Douglas R. Reno <renodr at linuxfromscratch dot org>
Date:                    2025-11-20
Initial Package Version: 1.14.5
Origin:                  Upstream (commits 372d9bc6b, 2fc439b7d, 2fc439b7d)
Upstream Status:         Applied
Description:             Fixes several known issues with SWIG 4.4.0 and Python
                         3.14, including memory leaks and crashes.

diff -Naurp subversion-1.14.5.orig/subversion/bindings/swig/include/svn_types.swg subversion-1.14.5/subversion/bindings/swig/include/svn_types.swg
--- subversion-1.14.5.orig/subversion/bindings/swig/include/svn_types.swg	2024-11-22 01:56:17.000000000 -0600
+++ subversion-1.14.5/subversion/bindings/swig/include/svn_types.swg	2025-11-19 20:24:40.999905161 -0600
@@ -601,7 +601,7 @@ svn_ ## TYPE ## _swig_rb_closed(VALUE se
 %typemap(in, noblock=1) apr_pool_t * {
   /* Verify that the user supplied a valid pool */
   if ($input != Py_None && $input != _global_py_pool) {
-    SWIG_Python_TypeError(SWIG_TypePrettyName($descriptor), $input);
+    SWIG_Error(SWIG_TypeError,  "a '$type' is expected");
     SWIG_arg_fail($svn_argnum);
     SWIG_fail;
   }
diff -Naurp subversion-1.14.5.orig/subversion/bindings/swig/python/tests/mergeinfo.py subversion-1.14.5/subversion/bindings/swig/python/tests/mergeinfo.py
--- subversion-1.14.5.orig/subversion/bindings/swig/python/tests/mergeinfo.py	2019-11-03 23:59:36.000000000 -0600
+++ subversion-1.14.5/subversion/bindings/swig/python/tests/mergeinfo.py	2025-11-19 20:24:35.837351707 -0600
@@ -18,7 +18,7 @@
 # under the License.
 #
 #
-import unittest, os, sys, gc
+import unittest, os, sys, weakref, gc
 from svn import core, repos, fs
 import utils
 
@@ -125,6 +125,9 @@ class SubversionMergeinfoTestCase(unitte
       }
     self.compare_mergeinfo_catalogs(mergeinfo, expected_mergeinfo)
 
+  @unittest.skipIf(utils.HAS_DEFERRED_REFCOUNT,
+                   "Reference counting tests skipped because of deferred "
+                   "reference counting")
   def test_mergeinfo_leakage__incorrect_range_t_refcounts(self):
     """Ensure that the ref counts on svn_merge_range_t objects returned by
        svn_mergeinfo_parse() are correct."""
@@ -138,7 +141,8 @@ class SubversionMergeinfoTestCase(unitte
         # ....and now 3 (incref during iteration of each range object)
 
         refcount = sys.getrefcount(r)
-        # ....and finally, 4 (getrefcount() also increfs)
+        # ....and finally, 4 (getrefcount() also increfs, unless deferred 
+        #                     reference counting)
         expected = 4
 
         # Note: if path and index are not '/trunk' and 0 respectively, then
@@ -150,8 +154,49 @@ class SubversionMergeinfoTestCase(unitte
           "cause: incorrect Py_INCREF/Py_DECREF usage in libsvn_swig_py/"
           "swigutil_py.c." % (expected, refcount, path, i)))
 
+  def test_mergeinfo_leakage__incorrect_range_t_weakrefs(self):
+    """Ensure that the ref counts on svn_merge_range_t objects returned by
+       svn_mergeinfo_parse() are correct."""
+    # When reference counting is working properly, each svn_merge_range_t in
+    # the returned mergeinfo will have a ref count of 1...
+    mergeinfo = core.svn_mergeinfo_parse(self.TEXT_MERGEINFO1)
+    merge_range_refdict = weakref.WeakValueDictionary()
+    merge_range_indexes = []
+    n_merge_range = 0 
+    for (path, rangelist) in core._as_list(mergeinfo.items()):
+      # ....and now 2 (incref during iteration of rangelist)
+
+      for (i, r) in enumerate(rangelist):
+        # ....and now 3 (incref during iteration of each range object)
+
+        idx = (path, i)
+        merge_range_refdict[idx] = r
+        merge_range_indexes.append(idx) 
+        n_merge_range += 1
+
+        # Note: if path and index are not '/trunk' and 0 respectively, then
+        # only some of the range objects are leaking, which is, as far as
+        # leaks go, even more impressive.
+
+    del rangelist, r
+    gc.collect()
+    # Now (strong) reference count of all svn_merge_range_t should be 1
+    # again and those objects should not be removed yet.
+    for idx in merge_range_indexes:
+      self.assertIn(idx, merge_range_refdict, (
+          "Refarence count error on svn_merge_info_t object for "
+          "(path: %s, index: %d). It should still exists because "
+          "mergeinfo holds its reference, but after GC, it already "
+          "removed." % idx))
     del mergeinfo
     gc.collect()
+    if merge_range_refdict:
+      # certainly memory leak, but we want to listing up leaked objects
+      # before raise an assertion error.  
+      self.assertFalse(merge_range_refdict,
+         "Memory leak! All svn_merge_range_t object holded "
+         "by mergeinfo object should be removed, but at least "
+         "one object still alive.")
 
   def test_mergeinfo_leakage__lingering_range_t_objects_after_del(self):
     """Ensure that there are no svn_merge_range_t objects being tracked by
@@ -162,6 +207,9 @@ class SubversionMergeinfoTestCase(unitte
        objects will be garbage collected and thus, not appear in the list of
        objects returned by gc.get_objects()."""
     mergeinfo = core.svn_mergeinfo_parse(self.TEXT_MERGEINFO1)
+    lingering = get_svn_merge_range_t_objects()
+    self.assertNotEqual(lingering, list())
+    del lingering
     del mergeinfo
     gc.collect()
     lingering = get_svn_merge_range_t_objects()
diff -Naurp subversion-1.14.5.orig/subversion/bindings/swig/python/tests/repository.py subversion-1.14.5/subversion/bindings/swig/python/tests/repository.py
--- subversion-1.14.5.orig/subversion/bindings/swig/python/tests/repository.py	2024-01-19 22:00:04.000000000 -0600
+++ subversion-1.14.5/subversion/bindings/swig/python/tests/repository.py	2025-11-19 20:24:35.837802341 -0600
@@ -87,15 +87,32 @@ class DumpStreamParser(repos.ParseFns3):
 
 class BatonCollector(repos.ChangeCollector):
   """A ChangeCollector with collecting batons, too"""
+
   def __init__(self, fs_ptr, root, pool=None, notify_cb=None):
+
+    def get_expected_baton_refcount():
+      """determine expected refcount of batons within a batoun_tuple,
+         by using dumy object"""
+      self.open_root(-1, None)
+      for baton_tuple in self.batons: 
+        rc = sys.getrefcount(baton_tuple[2])
+        break
+      return rc
+    
     repos.ChangeCollector.__init__(self, fs_ptr, root, pool, notify_cb)
-    self.batons = []
     self.close_called = False
     self.abort_called = False
+    # temporary values for get_expected_baton_refcount
+    self.batons = []
+    self.expected_baton_refcount = 0
+    # determin expected_baton_refcount
+    self.expected_baton_refcount = get_expected_baton_refcount()
+    # re-initialize the values after calling get_expected_baton_refcount()
+    self.batons = []
 
   def open_root(self, base_revision, dir_pool=None):
     bt = repos.ChangeCollector.open_root(self, base_revision, dir_pool)
-    self.batons.append((b'dir baton', b'', bt, sys.getrefcount(bt)))
+    self.batons.append((b'dir baton', b'', bt, self.expected_baton_refcount))
     return bt
 
   def add_directory(self, path, parent_baton,
@@ -104,14 +121,14 @@ class BatonCollector(repos.ChangeCollect
                                              copyfrom_path,
                                              copyfrom_revision,
                                              dir_pool)
-    self.batons.append((b'dir baton', path, bt, sys.getrefcount(bt)))
+    self.batons.append((b'dir baton', path, bt, self.expected_baton_refcount))
     return bt
 
   def open_directory(self, path, parent_baton, base_revision,
                      dir_pool=None):
     bt = repos.ChangeCollector.open_directory(self, path, parent_baton,
                                               base_revision, dir_pool)
-    self.batons.append((b'dir baton', path, bt, sys.getrefcount(bt)))
+    self.batons.append((b'dir baton', path, bt, self.expected_baton_refcount))
     return bt
 
   def add_file(self, path, parent_baton,
@@ -119,13 +136,13 @@ class BatonCollector(repos.ChangeCollect
     bt = repos.ChangeCollector.add_file(self, path, parent_baton,
                                         copyfrom_path, copyfrom_revision,
                                         file_pool)
-    self.batons.append((b'file baton', path, bt, sys.getrefcount(bt)))
+    self.batons.append((b'file baton', path, bt, self.expected_baton_refcount))
     return bt
 
   def open_file(self, path, parent_baton, base_revision, file_pool=None):
     bt = repos.ChangeCollector.open_file(self, path, parent_baton,
                                          base_revision, file_pool)
-    self.batons.append((b'file baton', path, bt, sys.getrefcount(bt)))
+    self.batons.append((b'file baton', path, bt, self.expected_baton_refcount))
     return bt
 
   def close_edit(self, pool=None):
@@ -429,29 +446,33 @@ class SubversionRepositoryTestCase(unitt
     root = fs.revision_root(self.fs, self.rev)
     editor = BatonCollector(self.fs, root)
     e_ptr, e_baton = delta.make_editor(editor)
+    refcount_at_first = sys.getrefcount(e_ptr)
     repos.replay(root, e_ptr, e_baton)
-    for baton in editor.batons:
-      self.assertEqual(sys.getrefcount(baton[2]), 2,
+    for baton_tuple in editor.batons:
+      # baton_tuple: 4-tuple(baton_type: bytes, node: bytes, bt: baton,
+      #                      expected_refcount_of_bt: int)
+      self.assertEqual(sys.getrefcount(baton_tuple[2]), baton_tuple[3],
                        "leak on baton %s after replay without errors"
-                       % repr(baton))
+                       % repr(baton_tuple))
     del e_baton
-    self.assertEqual(sys.getrefcount(e_ptr), 2,
+    self.assertEqual(sys.getrefcount(e_ptr), refcount_at_first,
                      "leak on editor baton after replay without errors")
 
     editor = BatonCollectorErrorOnClose(self.fs, root,
                                         error_path=b'branches/v1x')
     e_ptr, e_baton = delta.make_editor(editor)
+    refcount_at_first = sys.getrefcount(e_ptr)
     self.assertRaises(SubversionException, repos.replay, root, e_ptr, e_baton)
     batons = editor.batons
     # As svn_repos_replay calls neither close_edit callback nor abort_edit
     # if an error has occured during processing, references of Python objects
     # in decendant batons may live until e_baton is deleted.
     del e_baton
-    for baton in batons:
-      self.assertEqual(sys.getrefcount(baton[2]), 2,
+    for baton_tuple in batons:
+      self.assertEqual(sys.getrefcount(baton_tuple[2]), baton_tuple[3],
                        "leak on baton %s after replay with an error"
-                       % repr(baton))
-    self.assertEqual(sys.getrefcount(e_ptr), 2,
+                       % repr(baton_tuple))
+    self.assertEqual(sys.getrefcount(e_ptr), refcount_at_first,
                      "leak on editor baton after replay with an error")
 
   def test_delta_editor_apply_textdelta_handler_refcount(self):
diff -Naurp subversion-1.14.5.orig/subversion/bindings/swig/python/tests/utils.py subversion-1.14.5/subversion/bindings/swig/python/tests/utils.py
--- subversion-1.14.5.orig/subversion/bindings/swig/python/tests/utils.py	2022-03-27 09:53:32.000000000 -0500
+++ subversion-1.14.5/subversion/bindings/swig/python/tests/utils.py	2025-11-19 20:24:35.838127789 -0600
@@ -95,3 +95,13 @@ def codecs_eq(a, b):
 
 def is_defaultencoding_utf8():
   return codecs_eq(sys.getdefaultencoding(), 'utf-8')
+
+def get_holded_refcount_by_getrefcount():
+  "get refcount holded by sys.getrefcount() if its arg is a local variable"
+  a = []
+  rv = sys.getrefcount(a) - 1
+  return rv
+
+HAS_DEFERRED_REFCOUNT = not get_holded_refcount_by_getrefcount()
+
+del get_holded_refcount_by_getrefcount
diff -Naurp subversion-1.14.5.orig/subversion/bindings/swig/svn_delta.i subversion-1.14.5/subversion/bindings/swig/svn_delta.i
--- subversion-1.14.5.orig/subversion/bindings/swig/svn_delta.i	2023-10-19 23:00:04.000000000 -0500
+++ subversion-1.14.5/subversion/bindings/swig/svn_delta.i	2025-11-19 20:24:38.850833031 -0600
@@ -208,6 +208,7 @@ void _ops_get(int *num_ops, const svn_tx
 # Baton container class for editor/parse_fns3 batons and their decendants.
 class _ItemBaton:
   def __init__(self, editor, pool, baton=None):
+    import libsvn.core
     self.pool = pool if pool else libsvn.core.svn_pool_create()
     self.baton = baton
     self.editor = editor
