diff -pruN 0.2-1/.github/dependabot.yaml 0.3.7-1/.github/dependabot.yaml
--- 0.2-1/.github/dependabot.yaml	1970-01-01 00:00:00.000000000 +0000
+++ 0.3.7-1/.github/dependabot.yaml	2025-11-04 15:53:48.000000000 +0000
@@ -0,0 +1,13 @@
+# Keep GitHub Actions up to date with GitHub's Dependabot...
+# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
+# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem
+version: 2
+updates:
+  - package-ecosystem: "github-actions"
+    directory: "/"
+    schedule:
+      interval: weekly
+  - package-ecosystem: "pip"
+    directory: "/"
+    schedule:
+      interval: weekly
diff -pruN 0.2-1/.github/workflows/auto-merge.yml 0.3.7-1/.github/workflows/auto-merge.yml
--- 0.2-1/.github/workflows/auto-merge.yml	1970-01-01 00:00:00.000000000 +0000
+++ 0.3.7-1/.github/workflows/auto-merge.yml	2025-11-04 15:53:48.000000000 +0000
@@ -0,0 +1,22 @@
+name: Dependabot auto-merge
+on: pull_request_target
+
+permissions:
+  pull-requests: write
+  contents: write
+
+jobs:
+  dependabot:
+    runs-on: ubuntu-latest
+    if: ${{ github.actor == 'dependabot[bot]' }}
+    steps:
+      - name: Dependabot metadata
+        id: metadata
+        uses: dependabot/fetch-metadata@v2
+        with:
+          github-token: "${{ secrets.GITHUB_TOKEN }}"
+      - name: Enable auto-merge for Dependabot PRs
+        run: gh pr merge --auto --squash "$PR_URL"
+        env:
+          PR_URL: ${{github.event.pull_request.html_url}}
+          GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
diff -pruN 0.2-1/.github/workflows/disperse.yml 0.3.7-1/.github/workflows/disperse.yml
--- 0.2-1/.github/workflows/disperse.yml	1970-01-01 00:00:00.000000000 +0000
+++ 0.3.7-1/.github/workflows/disperse.yml	2025-11-04 15:53:48.000000000 +0000
@@ -0,0 +1,14 @@
+---
+name: Disperse configuration
+
+"on":
+  - push
+
+jobs:
+  build:
+
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v5
+      - uses: jelmer/action-disperse-validate@v2
diff -pruN 0.2-1/.github/workflows/pythonpackage.yml 0.3.7-1/.github/workflows/pythonpackage.yml
--- 0.2-1/.github/workflows/pythonpackage.yml	1970-01-01 00:00:00.000000000 +0000
+++ 0.3.7-1/.github/workflows/pythonpackage.yml	2025-11-04 15:53:48.000000000 +0000
@@ -0,0 +1,45 @@
+---
+name: Python package
+
+"on": [push, pull_request]
+
+jobs:
+  build:
+
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix:
+        os: [ubuntu-latest, macos-latest, windows-latest]
+        python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
+      fail-fast: false
+
+    steps:
+      - uses: actions/checkout@v5
+      - name: Set up Python ${{ matrix.python-version }}
+        uses: actions/setup-python@v6
+        with:
+          python-version: ${{ matrix.python-version }}
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip cython
+          pip install -U pip ".[dev]"
+      - name: Style checks
+        run: |
+          python -m ruff check .
+          python -m ruff format --check .
+      - name: Test suite run (pure Python)
+        run: |
+          python -m unittest tests.test_suite
+        env:
+          PYTHONHASHSEED: random
+      - name: Run cargo fmt
+        run: |
+          cargo fmt --all -- --check
+      - name: Install in editable mode
+        run: |
+          pip install -e .
+      - name: Test suite run (with Rust extension)
+        run: |
+          python -m unittest tests.test_suite
+        env:
+          PYTHONHASHSEED: random
diff -pruN 0.2-1/.github/workflows/wheels.yaml 0.3.7-1/.github/workflows/wheels.yaml
--- 0.2-1/.github/workflows/wheels.yaml	1970-01-01 00:00:00.000000000 +0000
+++ 0.3.7-1/.github/workflows/wheels.yaml	2025-11-04 15:53:48.000000000 +0000
@@ -0,0 +1,93 @@
+name: Build Python distributions
+
+on:
+  push:
+  pull_request:
+  schedule:
+    - cron: "0 6 * * *" # Daily 6AM UTC build
+
+jobs:
+  build-wheels:
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix:
+        os: [ubuntu-latest, macos-latest, windows-latest]
+      fail-fast: true
+
+    steps:
+      - uses: actions/checkout@v5
+      - uses: actions/setup-python@v6
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          pip install setuptools wheel cibuildwheel setuptools-rust
+      - name: Set up QEMU
+        uses: docker/setup-qemu-action@v3
+        if: "matrix.os == 'ubuntu-latest'"
+      - name: Build wheels
+        run: python -m cibuildwheel --output-dir wheelhouse
+      - name: Upload wheels
+        uses: actions/upload-artifact@v5
+        with:
+          name: artifact-${{ matrix.os }}
+          path: ./wheelhouse/*.whl
+
+  build-sdist:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v5
+      - uses: actions/setup-python@v6
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          pip install build
+      - name: Build sdist
+        run: python -m build --sdist
+      - name: Upload sdist
+        uses: actions/upload-artifact@v5
+        with:
+          name: artifact-sdist
+          path: ./dist/*.tar.gz
+
+  test-sdist:
+    needs:
+      - build-sdist
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/setup-python@v6
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          # Upgrade packging to avoid a bug in twine.
+          # See https://github.com/pypa/twine/issues/1216
+          pip install "twine>=6.1.0" "packaging>=24.2"
+      - name: Download sdist
+        uses: actions/download-artifact@v5
+        with:
+          name: artifact-sdist
+          path: dist
+      - name: Test sdist
+        run: twine check dist/*
+      - name: Test installation from sdist
+        run: pip install dist/*.tar.gz
+
+  publish:
+    runs-on: ubuntu-latest
+    needs:
+      - build-wheels
+      - build-sdist
+    if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
+    permissions:
+      id-token: write
+    environment:
+      name: pypi
+      url: https://pypi.org/p/fastbencode
+    steps:
+      - name: Download distributions
+        uses: actions/download-artifact@v5
+        with:
+          merge-multiple: true
+          pattern: artifact-*
+          path: dist
+      - name: Publish package distributions to PyPI
+        uses: pypa/gh-action-pypi-publish@release/v1
diff -pruN 0.2-1/.gitignore 0.3.7-1/.gitignore
--- 0.2-1/.gitignore	1970-01-01 00:00:00.000000000 +0000
+++ 0.3.7-1/.gitignore	2025-11-04 15:53:48.000000000 +0000
@@ -0,0 +1,15 @@
+*.so
+build
+__pycache__
+fastbencode.egg-info
+*.pyc
+dist
+*~
+.mypy_cache
+*.swp
+*.swo
+*.swn
+
+# Rust
+target/
+**/*.rs.bk
diff -pruN 0.2-1/CODE_OF_CONDUCT.md 0.3.7-1/CODE_OF_CONDUCT.md
--- 0.2-1/CODE_OF_CONDUCT.md	2021-08-20 09:58:06.000000000 +0000
+++ 0.3.7-1/CODE_OF_CONDUCT.md	2025-11-04 15:53:48.000000000 +0000
@@ -6,7 +6,7 @@ In the interest of fostering an open and
 contributors and maintainers pledge to making participation in our project and
 our community a harassment-free experience for everyone, regardless of age, body
 size, disability, ethnicity, sex characteristics, gender identity and expression,
-level of experience, education, socio-economic status, nationality, personal
+level of experience, education, socioeconomic status, nationality, personal
 appearance, race, religion, or sexual identity and orientation.
 
 ## Our Standards
diff -pruN 0.2-1/COPYING 0.3.7-1/COPYING
--- 0.2-1/COPYING	2021-08-20 09:58:06.000000000 +0000
+++ 0.3.7-1/COPYING	2025-11-04 15:53:48.000000000 +0000
@@ -1,339 +1,202 @@
-                    GNU GENERAL PUBLIC LICENSE
-                       Version 2, June 1991
 
- Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
- 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
-                            Preamble
-
-  The licenses for most software are designed to take away your
-freedom to share and change it.  By contrast, the GNU General Public
-License is intended to guarantee your freedom to share and change free
-software--to make sure the software is free for all its users.  This
-General Public License applies to most of the Free Software
-Foundation's software and to any other program whose authors commit to
-using it.  (Some other Free Software Foundation software is covered by
-the GNU Lesser General Public License instead.)  You can apply it to
-your programs, too.
-
-  When we speak of free software, we are referring to freedom, not
-price.  Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-this service if you wish), that you receive source code or can get it
-if you want it, that you can change the software or use pieces of it
-in new free programs; and that you know you can do these things.
-
-  To protect your rights, we need to make restrictions that forbid
-anyone to deny you these rights or to ask you to surrender the rights.
-These restrictions translate to certain responsibilities for you if you
-distribute copies of the software, or if you modify it.
-
-  For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must give the recipients all the rights that
-you have.  You must make sure that they, too, receive or can get the
-source code.  And you must show them these terms so they know their
-rights.
-
-  We protect your rights with two steps: (1) copyright the software, and
-(2) offer you this license which gives you legal permission to copy,
-distribute and/or modify the software.
-
-  Also, for each author's protection and ours, we want to make certain
-that everyone understands that there is no warranty for this free
-software.  If the software is modified by someone else and passed on, we
-want its recipients to know that what they have is not the original, so
-that any problems introduced by others will not reflect on the original
-authors' reputations.
-
-  Finally, any free program is threatened constantly by software
-patents.  We wish to avoid the danger that redistributors of a free
-program will individually obtain patent licenses, in effect making the
-program proprietary.  To prevent this, we have made it clear that any
-patent must be licensed for everyone's free use or not licensed at all.
-
-  The precise terms and conditions for copying, distribution and
-modification follow.
-
-                    GNU GENERAL PUBLIC LICENSE
-   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
-
-  0. This License applies to any program or other work which contains
-a notice placed by the copyright holder saying it may be distributed
-under the terms of this General Public License.  The "Program", below,
-refers to any such program or work, and a "work based on the Program"
-means either the Program or any derivative work under copyright law:
-that is to say, a work containing the Program or a portion of it,
-either verbatim or with modifications and/or translated into another
-language.  (Hereinafter, translation is included without limitation in
-the term "modification".)  Each licensee is addressed as "you".
-
-Activities other than copying, distribution and modification are not
-covered by this License; they are outside its scope.  The act of
-running the Program is not restricted, and the output from the Program
-is covered only if its contents constitute a work based on the
-Program (independent of having been made by running the Program).
-Whether that is true depends on what the Program does.
-
-  1. You may copy and distribute verbatim copies of the Program's
-source code as you receive it, in any medium, provided that you
-conspicuously and appropriately publish on each copy an appropriate
-copyright notice and disclaimer of warranty; keep intact all the
-notices that refer to this License and to the absence of any warranty;
-and give any other recipients of the Program a copy of this License
-along with the Program.
-
-You may charge a fee for the physical act of transferring a copy, and
-you may at your option offer warranty protection in exchange for a fee.
-
-  2. You may modify your copy or copies of the Program or any portion
-of it, thus forming a work based on the Program, and copy and
-distribute such modifications or work under the terms of Section 1
-above, provided that you also meet all of these conditions:
-
-    a) You must cause the modified files to carry prominent notices
-    stating that you changed the files and the date of any change.
-
-    b) You must cause any work that you distribute or publish, that in
-    whole or in part contains or is derived from the Program or any
-    part thereof, to be licensed as a whole at no charge to all third
-    parties under the terms of this License.
-
-    c) If the modified program normally reads commands interactively
-    when run, you must cause it, when started running for such
-    interactive use in the most ordinary way, to print or display an
-    announcement including an appropriate copyright notice and a
-    notice that there is no warranty (or else, saying that you provide
-    a warranty) and that users may redistribute the program under
-    these conditions, and telling the user how to view a copy of this
-    License.  (Exception: if the Program itself is interactive but
-    does not normally print such an announcement, your work based on
-    the Program is not required to print an announcement.)
-
-These requirements apply to the modified work as a whole.  If
-identifiable sections of that work are not derived from the Program,
-and can be reasonably considered independent and separate works in
-themselves, then this License, and its terms, do not apply to those
-sections when you distribute them as separate works.  But when you
-distribute the same sections as part of a whole which is a work based
-on the Program, the distribution of the whole must be on the terms of
-this License, whose permissions for other licensees extend to the
-entire whole, and thus to each and every part regardless of who wrote it.
-
-Thus, it is not the intent of this section to claim rights or contest
-your rights to work written entirely by you; rather, the intent is to
-exercise the right to control the distribution of derivative or
-collective works based on the Program.
-
-In addition, mere aggregation of another work not based on the Program
-with the Program (or with a work based on the Program) on a volume of
-a storage or distribution medium does not bring the other work under
-the scope of this License.
-
-  3. You may copy and distribute the Program (or a work based on it,
-under Section 2) in object code or executable form under the terms of
-Sections 1 and 2 above provided that you also do one of the following:
-
-    a) Accompany it with the complete corresponding machine-readable
-    source code, which must be distributed under the terms of Sections
-    1 and 2 above on a medium customarily used for software interchange; or,
-
-    b) Accompany it with a written offer, valid for at least three
-    years, to give any third party, for a charge no more than your
-    cost of physically performing source distribution, a complete
-    machine-readable copy of the corresponding source code, to be
-    distributed under the terms of Sections 1 and 2 above on a medium
-    customarily used for software interchange; or,
-
-    c) Accompany it with the information you received as to the offer
-    to distribute corresponding source code.  (This alternative is
-    allowed only for noncommercial distribution and only if you
-    received the program in object code or executable form with such
-    an offer, in accord with Subsection b above.)
-
-The source code for a work means the preferred form of the work for
-making modifications to it.  For an executable work, complete source
-code means all the source code for all modules it contains, plus any
-associated interface definition files, plus the scripts used to
-control compilation and installation of the executable.  However, as a
-special exception, the source code distributed need not include
-anything that is normally distributed (in either source or binary
-form) with the major components (compiler, kernel, and so on) of the
-operating system on which the executable runs, unless that component
-itself accompanies the executable.
-
-If distribution of executable or object code is made by offering
-access to copy from a designated place, then offering equivalent
-access to copy the source code from the same place counts as
-distribution of the source code, even though third parties are not
-compelled to copy the source along with the object code.
-
-  4. You may not copy, modify, sublicense, or distribute the Program
-except as expressly provided under this License.  Any attempt
-otherwise to copy, modify, sublicense or distribute the Program is
-void, and will automatically terminate your rights under this License.
-However, parties who have received copies, or rights, from you under
-this License will not have their licenses terminated so long as such
-parties remain in full compliance.
-
-  5. You are not required to accept this License, since you have not
-signed it.  However, nothing else grants you permission to modify or
-distribute the Program or its derivative works.  These actions are
-prohibited by law if you do not accept this License.  Therefore, by
-modifying or distributing the Program (or any work based on the
-Program), you indicate your acceptance of this License to do so, and
-all its terms and conditions for copying, distributing or modifying
-the Program or works based on it.
-
-  6. Each time you redistribute the Program (or any work based on the
-Program), the recipient automatically receives a license from the
-original licensor to copy, distribute or modify the Program subject to
-these terms and conditions.  You may not impose any further
-restrictions on the recipients' exercise of the rights granted herein.
-You are not responsible for enforcing compliance by third parties to
-this License.
-
-  7. If, as a consequence of a court judgment or allegation of patent
-infringement or for any other reason (not limited to patent issues),
-conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License.  If you cannot
-distribute so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you
-may not distribute the Program at all.  For example, if a patent
-license would not permit royalty-free redistribution of the Program by
-all those who receive copies directly or indirectly through you, then
-the only way you could satisfy both it and this License would be to
-refrain entirely from distribution of the Program.
-
-If any portion of this section is held invalid or unenforceable under
-any particular circumstance, the balance of the section is intended to
-apply and the section as a whole is intended to apply in other
-circumstances.
-
-It is not the purpose of this section to induce you to infringe any
-patents or other property right claims or to contest validity of any
-such claims; this section has the sole purpose of protecting the
-integrity of the free software distribution system, which is
-implemented by public license practices.  Many people have made
-generous contributions to the wide range of software distributed
-through that system in reliance on consistent application of that
-system; it is up to the author/donor to decide if he or she is willing
-to distribute software through any other system and a licensee cannot
-impose that choice.
-
-This section is intended to make thoroughly clear what is believed to
-be a consequence of the rest of this License.
-
-  8. If the distribution and/or use of the Program is restricted in
-certain countries either by patents or by copyrighted interfaces, the
-original copyright holder who places the Program under this License
-may add an explicit geographical distribution limitation excluding
-those countries, so that distribution is permitted only in or among
-countries not thus excluded.  In such case, this License incorporates
-the limitation as if written in the body of this License.
-
-  9. The Free Software Foundation may publish revised and/or new versions
-of the General Public License from time to time.  Such new versions will
-be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
-Each version is given a distinguishing version number.  If the Program
-specifies a version number of this License which applies to it and "any
-later version", you have the option of following the terms and conditions
-either of that version or of any later version published by the Free
-Software Foundation.  If the Program does not specify a version number of
-this License, you may choose any version ever published by the Free Software
-Foundation.
-
-  10. If you wish to incorporate parts of the Program into other free
-programs whose distribution conditions are different, write to the author
-to ask for permission.  For software which is copyrighted by the Free
-Software Foundation, write to the Free Software Foundation; we sometimes
-make exceptions for this.  Our decision will be guided by the two goals
-of preserving the free status of all derivatives of our free software and
-of promoting the sharing and reuse of software generally.
-
-                            NO WARRANTY
-
-  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
-FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
-OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
-PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
-OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
-MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
-TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
-PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
-REPAIR OR CORRECTION.
-
-  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
-REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
-INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
-OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
-TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
-YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
-PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
-POSSIBILITY OF SUCH DAMAGES.
-
-                     END OF TERMS AND CONDITIONS
-
-            How to Apply These Terms to Your New Programs
-
-  If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
-  To do so, attach the following notices to the program.  It is safest
-to attach them to the start of each source file to most effectively
-convey the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
-    <one line to give the program's name and a brief idea of what it does.>
-    Copyright (C) <year>  <name of author>
-
-    This program is free software; you can redistribute it and/or modify
-    it under the terms of the GNU General Public License as published by
-    the Free Software Foundation; either version 2 of the License, or
-    (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU General Public License for more details.
-
-    You should have received a copy of the GNU General Public License along
-    with this program; if not, write to the Free Software Foundation, Inc.,
-    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
-
-Also add information on how to contact you by electronic and paper mail.
-
-If the program is interactive, make it output a short notice like this
-when it starts in an interactive mode:
-
-    Gnomovision version 69, Copyright (C) year name of author
-    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
-    This is free software, and you are welcome to redistribute it
-    under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License.  Of course, the commands you use may
-be called something other than `show w' and `show c'; they could even be
-mouse-clicks or menu items--whatever suits your program.
-
-You should also get your employer (if you work as a programmer) or your
-school, if any, to sign a "copyright disclaimer" for the program, if
-necessary.  Here is a sample; alter the names:
-
-  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
-  `Gnomovision' (which makes passes at compilers) written by James Hacker.
-
-  <signature of Ty Coon>, 1 April 1989
-  Ty Coon, President of Vice
-
-This General Public License does not permit incorporating your program into
-proprietary programs.  If your program is a subroutine library, you may
-consider it more useful to permit linking proprietary applications with the
-library.  If this is what you want to do, use the GNU Lesser General
-Public License instead of this License.
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff -pruN 0.2-1/Cargo.lock 0.3.7-1/Cargo.lock
--- 0.2-1/Cargo.lock	1970-01-01 00:00:00.000000000 +0000
+++ 0.3.7-1/Cargo.lock	2025-11-04 15:53:48.000000000 +0000
@@ -0,0 +1,172 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "fastbencode"
+version = "0.3.7"
+dependencies = [
+ "pyo3",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "indoc"
+version = "2.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
+dependencies = [
+ "rustversion",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.177"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
+
+[[package]]
+name = "memoffset"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+[[package]]
+name = "portable-atomic"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "pyo3"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ba0117f4212101ee6544044dae45abe1083d30ce7b29c4b5cbdfa2354e07383"
+dependencies = [
+ "indoc",
+ "libc",
+ "memoffset",
+ "once_cell",
+ "portable-atomic",
+ "pyo3-build-config",
+ "pyo3-ffi",
+ "pyo3-macros",
+ "unindent",
+]
+
+[[package]]
+name = "pyo3-build-config"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fc6ddaf24947d12a9aa31ac65431fb1b851b8f4365426e182901eabfb87df5f"
+dependencies = [
+ "target-lexicon",
+]
+
+[[package]]
+name = "pyo3-ffi"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "025474d3928738efb38ac36d4744a74a400c901c7596199e20e45d98eb194105"
+dependencies = [
+ "libc",
+ "pyo3-build-config",
+]
+
+[[package]]
+name = "pyo3-macros"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e64eb489f22fe1c95911b77c44cc41e7c19f3082fc81cce90f657cdc42ffded"
+dependencies = [
+ "proc-macro2",
+ "pyo3-macros-backend",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pyo3-macros-backend"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "100246c0ecf400b475341b8455a9213344569af29a3c841d29270e53102e0fcf"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "pyo3-build-config",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "syn"
+version = "2.0.108"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "target-lexicon"
+version = "0.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
+
+[[package]]
+name = "unindent"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
diff -pruN 0.2-1/Cargo.toml 0.3.7-1/Cargo.toml
--- 0.2-1/Cargo.toml	1970-01-01 00:00:00.000000000 +0000
+++ 0.3.7-1/Cargo.toml	2025-11-04 15:53:48.000000000 +0000
@@ -0,0 +1,17 @@
+[package]
+name = "fastbencode"
+version = "0.3.7"
+edition = "2021"
+authors = ["Jelmer Vernooĳ <jelmer@jelmer.uk>"]
+license = "Apache-2.0"
+description = "Implementation of bencode with Rust implementation"
+readme = "README.md"
+repository = "https://github.com/breezy-team/fastbencode"
+publish = false
+
+[lib]
+name = "fastbencode__bencode_rs"
+crate-type = ["cdylib"]
+
+[dependencies]
+pyo3 = { version = ">=0.25,<0.27", features = ["extension-module"] }
diff -pruN 0.2-1/MANIFEST.in 0.3.7-1/MANIFEST.in
--- 0.2-1/MANIFEST.in	2023-02-11 13:50:44.000000000 +0000
+++ 0.3.7-1/MANIFEST.in	2025-11-04 15:53:48.000000000 +0000
@@ -1,4 +1,6 @@
 include README.md
 include COPYING
 include fastbencode/py.typed
-recursive-include fastbencode *.py *.pyx *.pxd *.h *.c
+include Cargo.toml
+recursive-include tests *.py
+recursive-include src *.rs
diff -pruN 0.2-1/PKG-INFO 0.3.7-1/PKG-INFO
--- 0.2-1/PKG-INFO	2023-02-11 13:50:44.000000000 +0000
+++ 0.3.7-1/PKG-INFO	1970-01-01 00:00:00.000000000 +0000
@@ -1,50 +0,0 @@
-Metadata-Version: 2.1
-Name: fastbencode
-Version: 0.2
-Summary: Implementation of bencode with optional fast C extensions
-Home-page: https://github.com/breezy-team/fastbencode
-Maintainer: Breezy Developers
-Maintainer-email: breezy-core@googlegroups.com
-License: GPLv2 or later
-Project-URL: GitHub, https://github.com/breezy-team/fastbencode
-Classifier: Programming Language :: Python :: 3.7
-Classifier: Programming Language :: Python :: 3.8
-Classifier: Programming Language :: Python :: 3.9
-Classifier: Programming Language :: Python :: 3.10
-Classifier: Programming Language :: Python :: 3.11
-Classifier: Programming Language :: Python :: Implementation :: CPython
-Classifier: Programming Language :: Python :: Implementation :: PyPy
-Classifier: Operating System :: POSIX
-Classifier: Operating System :: Microsoft :: Windows
-Requires-Python: >=3.7
-Provides-Extra: cext
-License-File: COPYING
-
-fastbencode
-===========
-
-fastbencode is an implementation of the bencode serialization format originally
-used by BitTorrent.
-
-The package includes both a pure-Python version and an optional C extension
-based on Cython.  Both provide the same functionality, but the C extension
-provides significantly better performance.
-
-Example:
-
-    >>> from fastbencode import bencode, bdecode
-    >>> bencode([1, 2, b'a', {b'd': 3}])
-    b'li1ei2e1:ad1:di3eee'
-    >>> bdecode(bencode([1, 2, b'a', {b'd': 3}]))
-    [1, 2, b'a', {b'd': 3}]
-
-License
-=======
-fastbencode is available under the GNU GPL, version 2 or later.
-
-Copyright
-=========
-
-* Original Pure-Python bencoder (c) Petru Paler
-* Cython version and modifications (c) Canonical Ltd
-* Split out from Bazaar/Breezy by Jelmer Vernooĳ
diff -pruN 0.2-1/README.md 0.3.7-1/README.md
--- 0.2-1/README.md	2022-09-24 23:00:54.000000000 +0000
+++ 0.3.7-1/README.md	2025-11-04 15:53:48.000000000 +0000
@@ -4,8 +4,8 @@ fastbencode
 fastbencode is an implementation of the bencode serialization format originally
 used by BitTorrent.
 
-The package includes both a pure-Python version and an optional C extension
-based on Cython.  Both provide the same functionality, but the C extension
+The package includes both a pure-Python version and an optional Rust extension
+based on PyO3. Both provide the same functionality, but the Rust extension
 provides significantly better performance.
 
 Example:
@@ -16,13 +16,19 @@ Example:
     >>> bdecode(bencode([1, 2, b'a', {b'd': 3}]))
     [1, 2, b'a', {b'd': 3}]
 
+The default ``bencode``/``bdecode`` functions just operate on
+bytestrings. Use ``bencode_utf8`` / ``bdecode_utf8`` to
+serialize/deserialize all plain strings as UTF-8 bytestrings.
+Note that for performance reasons, all dictionary keys still have to be
+bytestrings.
+
 License
 =======
-fastbencode is available under the GNU GPL, version 2 or later.
+fastbencode is available under the Apache License, version 2.
 
 Copyright
 =========
 
-* Original Pure-Python bencoder (c) Petru Paler
-* Cython version and modifications (c) Canonical Ltd
+* Original Pure-Python bencoder © Petru Paler
 * Split out from Bazaar/Breezy by Jelmer Vernooĳ
+* Rust extension © Jelmer Vernooĳ
diff -pruN 0.2-1/debian/cargo_home/config.toml 0.3.7-1/debian/cargo_home/config.toml
--- 0.2-1/debian/cargo_home/config.toml	1970-01-01 00:00:00.000000000 +0000
+++ 0.3.7-1/debian/cargo_home/config.toml	2025-10-30 13:35:24.000000000 +0000
@@ -0,0 +1,7 @@
+[source]
+
+[source.mirror]
+directory = "/usr/share/cargo/registry/"
+
+[source.crates-io]
+replace-with = "mirror"
diff -pruN 0.2-1/debian/changelog 0.3.7-1/debian/changelog
--- 0.2-1/debian/changelog	2023-02-11 14:03:04.000000000 +0000
+++ 0.3.7-1/debian/changelog	2025-11-04 10:04:23.000000000 +0000
@@ -1,3 +1,10 @@
+python-fastbencode (0.3.7-1) unstable; urgency=medium
+
+  * New upstream release.
+   + Migrates away from cython. Closes: #1119765
+
+ -- Jelmer Vernooĳ <jelmer@debian.org>  Tue, 04 Nov 2025 10:04:23 +0000
+
 python-fastbencode (0.2-1) unstable; urgency=low
 
   * New upstream release.
diff -pruN 0.2-1/debian/control 0.3.7-1/debian/control
--- 0.2-1/debian/control	2023-02-11 14:03:04.000000000 +0000
+++ 0.3.7-1/debian/control	2025-11-04 10:04:23.000000000 +0000
@@ -1,6 +1,6 @@
 Rules-Requires-Root: no
 Standards-Version: 4.5.1
-Build-Depends: debhelper-compat (= 13), dh-sequence-python3, python3-all-dev, cython3, python3-setuptools
+Build-Depends: debhelper-compat (= 13), dh-sequence-python3, python3-all-dev, cython3, python3-setuptools, cargo:native, libstd-rust-dev, rustc:native, librust-pyo3+default-dev (<< 0.27-~~), librust-pyo3+default-dev (>= 0.25-~~), librust-pyo3+extension-module-dev (<< 0.27-~~), librust-pyo3+extension-module-dev (>= 0.25-~~)
 Testsuite: autopkgtest-pkg-python
 Source: python-fastbencode
 Priority: optional
diff -pruN 0.2-1/debian/rules 0.3.7-1/debian/rules
--- 0.2-1/debian/rules	2023-02-11 14:03:04.000000000 +0000
+++ 0.3.7-1/debian/rules	2025-11-04 10:04:23.000000000 +0000
@@ -1,4 +1,19 @@
 #!/usr/bin/make -f
 
+export CARGO_HOME=$(shell pwd)/debian/cargo_home
+
 %:
 	dh $@ --buildsystem=pybuild
+
+update-deps:
+	# this required python3-debmutate
+	update-rust-deps --exclude-local-crates \
+		--drop-unreferenced
+
+override_dh_auto_clean:
+	-mv Cargo.lock.bak Cargo.lock || true
+	dh_auto_clean
+
+override_dh_auto_build:
+	-mv Cargo.lock Cargo.lock.bak || true
+	dh_auto_build
diff -pruN 0.2-1/disperse.conf 0.3.7-1/disperse.conf
--- 0.2-1/disperse.conf	2022-10-11 18:55:33.000000000 +0000
+++ 0.3.7-1/disperse.conf	1970-01-01 00:00:00.000000000 +0000
@@ -1,8 +0,0 @@
-# See https://github.com/jelmer/disperse
-timeout_days: 5
-tag_name: "v$VERSION"
-verify_command: "python3 -m unittest fastbencode.tests.test_suite"
-update_version {
-  path: "fastbencode/__init__.py"
-  new_line: '__version__ = $TUPLED_VERSION'
-}
diff -pruN 0.2-1/disperse.toml 0.3.7-1/disperse.toml
--- 0.2-1/disperse.toml	1970-01-01 00:00:00.000000000 +0000
+++ 0.3.7-1/disperse.toml	2025-11-04 15:53:48.000000000 +0000
@@ -0,0 +1,8 @@
+tag-name = "v$VERSION"
+verify-command = "python3 -m unittest fastbencode.tests.test_suite"
+tarball-location = []
+release-timeout = 5
+
+[[update_version]]
+path = "fastbencode/__init__.py"
+new-line = "__version__ = $TUPLED_VERSION"
diff -pruN 0.2-1/fastbencode/__init__.py 0.3.7-1/fastbencode/__init__.py
--- 0.2-1/fastbencode/__init__.py	2023-02-11 13:50:44.000000000 +0000
+++ 0.3.7-1/fastbencode/__init__.py	2025-11-04 15:53:48.000000000 +0000
@@ -1,4 +1,4 @@
-# Copyright (C) 2007, 2009 Canonical Ltd
+# Copyright (C) 2021-2023 Jelmer Vernooĳ <jelmer@jelmer.uk>
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -14,56 +14,35 @@
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
-"""Wrapper around the bencode cython and python implementation"""
+"""Wrapper around the bencode Rust and Python implementations."""
 
 from typing import Type
 
-
-__version__ = (0, 2)
-
-
-_extension_load_failures = []
-
-
-def failed_to_load_extension(exception):
-    """Handle failing to load a binary extension.
-
-    This should be called from the ImportError block guarding the attempt to
-    import the native extension.  If this function returns, the pure-Python
-    implementation should be loaded instead::
-
-    >>> try:
-    >>>     import _fictional_extension_pyx
-    >>> except ImportError, e:
-    >>>     failed_to_load_extension(e)
-    >>>     import _fictional_extension_py
-    """
-    # NB: This docstring is just an example, not a doctest, because doctest
-    # currently can't cope with the use of lazy imports in this namespace --
-    # mbp 20090729
-
-    # This currently doesn't report the failure at the time it occurs, because
-    # they tend to happen very early in startup when we can't check config
-    # files etc, and also we want to report all failures but not spam the user
-    # with 10 warnings.
-    exception_str = str(exception)
-    if exception_str not in _extension_load_failures:
-        import warnings
-        warnings.warn(
-            'failed to load compiled extension: %s' % exception_str,
-            UserWarning)
-        _extension_load_failures.append(exception_str)
+__version__ = (0, 3, 7)
 
 
 Bencached: Type
 
 try:
-    from ._bencode_pyx import bdecode, bdecode_as_tuple, bencode, Bencached
-except ImportError as e:
-    failed_to_load_extension(e)
-    from ._bencode_py import (  # noqa: F401
+    from fastbencode._bencode_rs import (
+        Bencached,
         bdecode,
         bdecode_as_tuple,
+        bdecode_utf8,
         bencode,
+        bencode_utf8,
+    )
+except ModuleNotFoundError as e:
+    import warnings
+
+    warnings.warn(f"failed to load compiled extension: {e}", UserWarning)
+
+    # Fall back to pure Python implementation
+    from ._bencode_py import (  # noqa: F401
         Bencached,
-        )
+        bdecode,
+        bdecode_as_tuple,
+        bdecode_utf8,
+        bencode,
+        bencode_utf8,
+    )
diff -pruN 0.2-1/fastbencode/_bencode_py.py 0.3.7-1/fastbencode/_bencode_py.py
--- 0.2-1/fastbencode/_bencode_py.py	2022-09-24 23:00:54.000000000 +0000
+++ 0.3.7-1/fastbencode/_bencode_py.py	2025-11-04 15:53:48.000000000 +0000
@@ -14,58 +14,62 @@
 # included in all copies or substantial portions of the Software.
 #
 # Modifications copyright (C) 2008 Canonical Ltd
+# Modifications copyright (C) 2021-2023 Jelmer Vernooĳ
 
 
-from typing import Dict, Type, Callable, List
+from typing import Callable, Dict, List, Type
 
 
-class BDecoder(object):
-
-    def __init__(self, yield_tuples=False):
+class BDecoder:
+    def __init__(self, yield_tuples=False, bytestring_encoding=None) -> None:
         """Constructor.
 
         :param yield_tuples: if true, decode "l" elements as tuples rather than
             lists.
         """
         self.yield_tuples = yield_tuples
+        self.bytestring_encoding = bytestring_encoding
         decode_func = {}
-        decode_func[b'l'] = self.decode_list
-        decode_func[b'd'] = self.decode_dict
-        decode_func[b'i'] = self.decode_int
-        decode_func[b'0'] = self.decode_string
-        decode_func[b'1'] = self.decode_string
-        decode_func[b'2'] = self.decode_string
-        decode_func[b'3'] = self.decode_string
-        decode_func[b'4'] = self.decode_string
-        decode_func[b'5'] = self.decode_string
-        decode_func[b'6'] = self.decode_string
-        decode_func[b'7'] = self.decode_string
-        decode_func[b'8'] = self.decode_string
-        decode_func[b'9'] = self.decode_string
+        decode_func[b"l"] = self.decode_list
+        decode_func[b"d"] = self.decode_dict
+        decode_func[b"i"] = self.decode_int
+        decode_func[b"0"] = self.decode_bytes
+        decode_func[b"1"] = self.decode_bytes
+        decode_func[b"2"] = self.decode_bytes
+        decode_func[b"3"] = self.decode_bytes
+        decode_func[b"4"] = self.decode_bytes
+        decode_func[b"5"] = self.decode_bytes
+        decode_func[b"6"] = self.decode_bytes
+        decode_func[b"7"] = self.decode_bytes
+        decode_func[b"8"] = self.decode_bytes
+        decode_func[b"9"] = self.decode_bytes
         self.decode_func = decode_func
 
     def decode_int(self, x, f):
         f += 1
-        newf = x.index(b'e', f)
+        newf = x.index(b"e", f)
         n = int(x[f:newf])
-        if x[f:f + 2] == b'-0':
+        if x[f : f + 2] == b"-0":
             raise ValueError
-        elif x[f:f + 1] == b'0' and newf != f + 1:
+        elif x[f : f + 1] == b"0" and newf != f + 1:
             raise ValueError
         return (n, newf + 1)
 
-    def decode_string(self, x, f):
-        colon = x.index(b':', f)
+    def decode_bytes(self, x, f):
+        colon = x.index(b":", f)
         n = int(x[f:colon])
-        if x[f:f + 1] == b'0' and colon != f + 1:
+        if x[f : f + 1] == b"0" and colon != f + 1:
             raise ValueError
         colon += 1
-        return (x[colon:colon + n], colon + n)
+        d = x[colon : colon + n]
+        if self.bytestring_encoding:
+            d = d.decode(self.bytestring_encoding)
+        return (d, colon + n)
 
     def decode_list(self, x, f):
         r, f = [], f + 1
-        while x[f:f + 1] != b'e':
-            v, f = self.decode_func[x[f:f + 1]](x, f)
+        while x[f : f + 1] != b"e":
+            v, f = self.decode_func[x[f : f + 1]](x, f)
             r.append(v)
         if self.yield_tuples:
             r = tuple(r)
@@ -74,12 +78,12 @@ class BDecoder(object):
     def decode_dict(self, x, f):
         r, f = {}, f + 1
         lastkey = None
-        while x[f:f + 1] != b'e':
-            k, f = self.decode_string(x, f)
+        while x[f : f + 1] != b"e":
+            k, f = self.decode_bytes(x, f)
             if lastkey is not None and lastkey >= k:
                 raise ValueError
             lastkey = k
-            r[k], f = self.decode_func[x[f:f + 1]](x, f)
+            r[k], f = self.decode_func[x[f : f + 1]](x, f)
         return (r, f + 1)
 
     def bdecode(self, x):
@@ -100,63 +104,82 @@ bdecode = _decoder.bdecode
 _tuple_decoder = BDecoder(True)
 bdecode_as_tuple = _tuple_decoder.bdecode
 
-
-class Bencached(object):
-    __slots__ = ['bencoded']
-
-    def __init__(self, s):
-        self.bencoded = s
-
-
-def encode_bencached(x, r):
-    r.append(x.bencoded)
-
-
-def encode_bool(x, r):
-    encode_int(int(x), r)
+_utf8_decoder = BDecoder(bytestring_encoding="utf-8")
+bdecode_utf8 = _utf8_decoder.bdecode
 
 
-def encode_int(x, r):
-    r.extend((b'i', int_to_bytes(x), b'e'))
-
-
-def encode_string(x, r):
-    r.extend((int_to_bytes(len(x)), b':', x))
-
-
-def encode_list(x, r):
-    r.append(b'l')
-    for i in x:
-        encode_func[type(i)](i, r)
-    r.append(b'e')
+class Bencached:
+    __slots__ = ["bencoded"]
 
+    def __init__(self, s) -> None:
+        self.bencoded = s
 
-def encode_dict(x, r):
-    r.append(b'd')
-    ilist = sorted(x.items())
-    for k, v in ilist:
-        r.extend((int_to_bytes(len(k)), b':', k))
-        encode_func[type(v)](v, r)
-    r.append(b'e')
 
+class BEncoder:
+    def __init__(self, bytestring_encoding=None):
+        self.bytestring_encoding = bytestring_encoding
+        self.encode_func: Dict[Type, Callable[[object, List[bytes]], None]] = {
+            Bencached: self.encode_bencached,
+            int: self.encode_int,
+            bytes: self.encode_bytes,
+            list: self.encode_list,
+            tuple: self.encode_list,
+            dict: self.encode_dict,
+            bool: self.encode_bool,
+            str: self.encode_str,
+        }
+
+    def encode_bencached(self, x, r):
+        r.append(x.bencoded)
+
+    def encode_bool(self, x, r):
+        self.encode_int(int(x), r)
+
+    def encode_int(self, x, r):
+        r.extend((b"i", int_to_bytes(x), b"e"))
+
+    def encode_bytes(self, x, r):
+        r.extend((int_to_bytes(len(x)), b":", x))
+
+    def encode_list(self, x, r):
+        r.append(b"l")
+        for i in x:
+            self.encode(i, r)
+        r.append(b"e")
+
+    def encode_dict(self, x, r):
+        r.append(b"d")
+        ilist = sorted(x.items())
+        for k, v in ilist:
+            r.extend((int_to_bytes(len(k)), b":", k))
+            self.encode(v, r)
+        r.append(b"e")
+
+    def encode_str(self, x, r):
+        if self.bytestring_encoding is None:
+            raise TypeError(
+                "string found but no encoding specified. "
+                "Use bencode_utf8 rather bencode?"
+            )
+        return self.encode_bytes(x.encode(self.bytestring_encoding), r)
 
-encode_func: Dict[Type, Callable[[object, List[bytes]], None]] = {}
-encode_func[type(Bencached(0))] = encode_bencached
-encode_func[int] = encode_int
+    def encode(self, x, r):
+        self.encode_func[type(x)](x, r)
 
 
 def int_to_bytes(n):
-    return b'%d' % n
+    return b"%d" % n
 
 
-encode_func[bytes] = encode_string
-encode_func[list] = encode_list
-encode_func[tuple] = encode_list
-encode_func[dict] = encode_dict
-encode_func[bool] = encode_bool
+def bencode(x):
+    r = []
+    encoder = BEncoder()
+    encoder.encode(x, r)
+    return b"".join(r)
 
 
-def bencode(x):
+def bencode_utf8(x):
     r = []
-    encode_func[type(x)](x, r)
-    return b''.join(r)
+    encoder = BEncoder(bytestring_encoding="utf-8")
+    encoder.encode(x, r)
+    return b"".join(r)
diff -pruN 0.2-1/fastbencode/_bencode_pyx.h 0.3.7-1/fastbencode/_bencode_pyx.h
--- 0.2-1/fastbencode/_bencode_pyx.h	2021-08-18 22:51:16.000000000 +0000
+++ 0.3.7-1/fastbencode/_bencode_pyx.h	1970-01-01 00:00:00.000000000 +0000
@@ -1,22 +0,0 @@
-/* Copyright (C) 2009 Canonical Ltd
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
- */
-
-/* Simple header providing some macro definitions for _bencode_pyx.pyx
- */
-
-#define D_UPDATE_TAIL(self, n) (((self)->size -= (n), (self)->tail += (n)))
-#define E_UPDATE_TAIL(self, n) (((self)->size += (n), (self)->tail += (n)))
diff -pruN 0.2-1/fastbencode/_bencode_pyx.pyi 0.3.7-1/fastbencode/_bencode_pyx.pyi
--- 0.2-1/fastbencode/_bencode_pyx.pyi	2022-09-07 13:15:29.000000000 +0000
+++ 0.3.7-1/fastbencode/_bencode_pyx.pyi	1970-01-01 00:00:00.000000000 +0000
@@ -1,7 +0,0 @@
-
-def bdecode(bytes) -> object: ...
-def bdecode_as_tuple(bytes) -> object: ...
-def bencode(object) -> bytes: ...
-
-
-class Bencached(object): ...
diff -pruN 0.2-1/fastbencode/_bencode_pyx.pyx 0.3.7-1/fastbencode/_bencode_pyx.pyx
--- 0.2-1/fastbencode/_bencode_pyx.pyx	2021-08-18 22:20:19.000000000 +0000
+++ 0.3.7-1/fastbencode/_bencode_pyx.pyx	1970-01-01 00:00:00.000000000 +0000
@@ -1,400 +0,0 @@
-# Copyright (C) 2007, 2009, 2010 Canonical Ltd
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-#
-# cython: language_level=3
-
-"""Pyrex implementation for bencode coder/decoder"""
-
-from cpython.bool cimport (
-    PyBool_Check,
-    )
-from cpython.bytes cimport (
-    PyBytes_CheckExact,
-    PyBytes_FromStringAndSize,
-    PyBytes_AS_STRING,
-    PyBytes_GET_SIZE,
-    )
-from cpython.dict cimport (
-    PyDict_CheckExact,
-    )
-from cpython.int cimport (
-    PyInt_CheckExact,
-    PyInt_FromString,
-    )
-from cpython.list cimport (
-    PyList_CheckExact,
-    PyList_Append,
-    )
-from cpython.long cimport (
-    PyLong_CheckExact,
-    )
-from cpython.mem cimport (
-    PyMem_Free,
-    PyMem_Malloc,
-    PyMem_Realloc,
-    )
-from cpython.tuple cimport (
-    PyTuple_CheckExact,
-    )
-
-from libc.stdlib cimport (
-    strtol,
-    )
-from libc.string cimport (
-    memcpy,
-    )
-
-cdef extern from "python-compat.h":
-    int snprintf(char* buffer, size_t nsize, char* fmt, ...)
-    # Use wrapper with inverted error return so Cython can propogate
-    int BrzPy_EnterRecursiveCall(char *) except 0
-
-cdef extern from "Python.h":
-    void Py_LeaveRecursiveCall()
-
-cdef class Decoder
-cdef class Encoder
-
-cdef extern from "_bencode_pyx.h":
-    void D_UPDATE_TAIL(Decoder, int n)
-    void E_UPDATE_TAIL(Encoder, int n)
-
-
-cdef class Decoder:
-    """Bencode decoder"""
-
-    cdef readonly char *tail
-    cdef readonly int size
-    cdef readonly int _yield_tuples
-    cdef object text
-
-    def __init__(self, s, yield_tuples=0):
-        """Initialize decoder engine.
-        @param  s:  Python string.
-        """
-        if not PyBytes_CheckExact(s):
-            raise TypeError("bytes required")
-
-        self.text = s
-        self.tail = PyBytes_AS_STRING(s)
-        self.size = PyBytes_GET_SIZE(s)
-        self._yield_tuples = int(yield_tuples)
-
-    def decode(self):
-        result = self._decode_object()
-        if self.size != 0:
-            raise ValueError('junk in stream')
-        return result
-
-    def decode_object(self):
-        return self._decode_object()
-
-    cdef object _decode_object(self):
-        cdef char ch
-
-        if 0 == self.size:
-            raise ValueError('stream underflow')
-
-        BrzPy_EnterRecursiveCall(" while bencode decoding")
-        try:
-            ch = self.tail[0]
-            if c'0' <= ch <= c'9':
-                return self._decode_string()
-            elif ch == c'l':
-                D_UPDATE_TAIL(self, 1)
-                return self._decode_list()
-            elif ch == c'i':
-                D_UPDATE_TAIL(self, 1)
-                return self._decode_int()
-            elif ch == c'd':
-                D_UPDATE_TAIL(self, 1)
-                return self._decode_dict()
-        finally:
-            Py_LeaveRecursiveCall()
-        raise ValueError('unknown object type identifier %r' % ch)
-
-    cdef int _read_digits(self, char stop_char) except -1:
-        cdef int i
-        i = 0
-        while ((self.tail[i] >= c'0' and self.tail[i] <= c'9') or
-               self.tail[i] == c'-') and i < self.size:
-            i = i + 1
-
-        if self.tail[i] != stop_char:
-            raise ValueError("Stop character %c not found: %c" % 
-                (stop_char, self.tail[i]))
-        if (self.tail[0] == c'0' or 
-                (self.tail[0] == c'-' and self.tail[1] == c'0')):
-            if i == 1:
-                return i
-            else:
-                raise ValueError # leading zeroes are not allowed
-        return i
-
-    cdef object _decode_int(self):
-        cdef int i
-        i = self._read_digits(c'e')
-        self.tail[i] = 0
-        try:
-            ret = PyInt_FromString(self.tail, NULL, 10)
-        finally:
-            self.tail[i] = c'e'
-        D_UPDATE_TAIL(self, i+1)
-        return ret
-
-    cdef object _decode_string(self):
-        cdef int n
-        cdef char *next_tail
-        # strtol allows leading whitespace, negatives, and leading zeros
-        # however, all callers have already checked that '0' <= tail[0] <= '9'
-        # or they wouldn't have called _decode_string
-        # strtol will stop at trailing whitespace, etc
-        n = strtol(self.tail, &next_tail, 10)
-        if next_tail == NULL or next_tail[0] != c':':
-            raise ValueError('string len not terminated by ":"')
-        # strtol allows leading zeros, so validate that we don't have that
-        if (self.tail[0] == c'0'
-            and (n != 0 or (next_tail - self.tail != 1))):
-            raise ValueError('leading zeros are not allowed')
-        D_UPDATE_TAIL(self, next_tail - self.tail + 1)
-        if n == 0:
-            return b''
-        if n > self.size:
-            raise ValueError('stream underflow')
-        if n < 0:
-            raise ValueError('string size below zero: %d' % n)
-
-        result = PyBytes_FromStringAndSize(self.tail, n)
-        D_UPDATE_TAIL(self, n)
-        return result
-
-    cdef object _decode_list(self):
-        result = []
-
-        while self.size > 0:
-            if self.tail[0] == c'e':
-                D_UPDATE_TAIL(self, 1)
-                if self._yield_tuples:
-                    return tuple(result)
-                else:
-                    return result
-            else:
-                # As a quick shortcut, check to see if the next object is a
-                # string, since we know that won't be creating recursion
-                # if self.tail[0] >= c'0' and self.tail[0] <= c'9':
-                PyList_Append(result, self._decode_object())
-
-        raise ValueError('malformed list')
-
-    cdef object _decode_dict(self):
-        cdef char ch
-
-        result = {}
-        lastkey = None
-
-        while self.size > 0:
-            ch = self.tail[0]
-            if ch == c'e':
-                D_UPDATE_TAIL(self, 1)
-                return result
-            else:
-                # keys should be strings only
-                if self.tail[0] < c'0' or self.tail[0] > c'9':
-                    raise ValueError('key was not a simple string.')
-                key = self._decode_string()
-                if lastkey is not None and lastkey >= key:
-                    raise ValueError('dict keys disordered')
-                else:
-                    lastkey = key
-                value = self._decode_object()
-                result[key] = value
-
-        raise ValueError('malformed dict')
-
-
-def bdecode(object s):
-    """Decode string x to Python object"""
-    return Decoder(s).decode()
-
-
-def bdecode_as_tuple(object s):
-    """Decode string x to Python object, using tuples rather than lists."""
-    return Decoder(s, True).decode()
-
-
-class Bencached(object):
-    __slots__ = ['bencoded']
-
-    def __init__(self, s):
-        self.bencoded = s
-
-
-cdef enum:
-    INITSIZE = 1024     # initial size for encoder buffer
-    INT_BUF_SIZE = 32
-
-
-cdef class Encoder:
-    """Bencode encoder"""
-
-    cdef readonly char *tail
-    cdef readonly int size
-    cdef readonly char *buffer
-    cdef readonly int maxsize
-
-    def __init__(self, int maxsize=INITSIZE):
-        """Initialize encoder engine
-        @param  maxsize:    initial size of internal char buffer
-        """
-        cdef char *p
-
-        self.maxsize = 0
-        self.size = 0
-        self.tail = NULL
-
-        p = <char*>PyMem_Malloc(maxsize)
-        if p == NULL:
-            raise MemoryError('Not enough memory to allocate buffer '
-                              'for encoder')
-        self.buffer = p
-        self.maxsize = maxsize
-        self.tail = p
-
-    def __dealloc__(self):
-        PyMem_Free(self.buffer)
-        self.buffer = NULL
-        self.maxsize = 0
-
-    def to_bytes(self):
-        if self.buffer != NULL and self.size != 0:
-            return PyBytes_FromStringAndSize(self.buffer, self.size)
-        return b''
-
-    cdef int _ensure_buffer(self, int required) except 0:
-        """Ensure that tail of CharTail buffer has enough size.
-        If buffer is not big enough then function try to
-        realloc buffer.
-        """
-        cdef char *new_buffer
-        cdef int   new_size
-
-        if self.size + required < self.maxsize:
-            return 1
-
-        new_size = self.maxsize
-        while new_size < self.size + required:
-            new_size = new_size * 2
-        new_buffer = <char*>PyMem_Realloc(self.buffer, <size_t>new_size)
-        if new_buffer == NULL:
-            raise MemoryError('Cannot realloc buffer for encoder')
-
-        self.buffer = new_buffer
-        self.maxsize = new_size
-        self.tail = &new_buffer[self.size]
-        return 1
-
-    cdef int _encode_int(self, int x) except 0:
-        """Encode int to bencode string iNNNe
-        @param  x:  value to encode
-        """
-        cdef int n
-        self._ensure_buffer(INT_BUF_SIZE)
-        n = snprintf(self.tail, INT_BUF_SIZE, b"i%de", x)
-        if n < 0:
-            raise MemoryError('int %d too big to encode' % x)
-        E_UPDATE_TAIL(self, n)
-        return 1
-
-    cdef int _encode_long(self, x) except 0:
-        return self._append_string(b'i%de' % x)
-
-    cdef int _append_string(self, s) except 0:
-        cdef Py_ssize_t n
-        n = PyBytes_GET_SIZE(s)
-        self._ensure_buffer(n)
-        memcpy(self.tail, PyBytes_AS_STRING(s), n)
-        E_UPDATE_TAIL(self, n)
-        return 1
-
-    cdef int _encode_string(self, x) except 0:
-        cdef int n
-        cdef Py_ssize_t x_len
-        x_len = PyBytes_GET_SIZE(x)
-        self._ensure_buffer(x_len + INT_BUF_SIZE)
-        n = snprintf(self.tail, INT_BUF_SIZE, b'%ld:', x_len)
-        if n < 0:
-            raise MemoryError('string %s too big to encode' % x)
-        memcpy(<void *>(self.tail+n), PyBytes_AS_STRING(x), x_len)
-        E_UPDATE_TAIL(self, n + x_len)
-        return 1
-
-    cdef int _encode_list(self, x) except 0:
-        self._ensure_buffer(1)
-        self.tail[0] = c'l'
-        E_UPDATE_TAIL(self, 1)
-
-        for i in x:
-            self.process(i)
-
-        self._ensure_buffer(1)
-        self.tail[0] = c'e'
-        E_UPDATE_TAIL(self, 1)
-        return 1
-
-    cdef int _encode_dict(self, x) except 0:
-        self._ensure_buffer(1)
-        self.tail[0] = c'd'
-        E_UPDATE_TAIL(self, 1)
-
-        for k in sorted(x):
-            if not PyBytes_CheckExact(k):
-                raise TypeError('key in dict should be string')
-            self._encode_string(k)
-            self.process(x[k])
-
-        self._ensure_buffer(1)
-        self.tail[0] = c'e'
-        E_UPDATE_TAIL(self, 1)
-        return 1
-
-    cpdef object process(self, object x):
-        BrzPy_EnterRecursiveCall(" while bencode encoding")
-        try:
-            if PyBytes_CheckExact(x):
-                self._encode_string(x)
-            elif PyInt_CheckExact(x) and x.bit_length() < 32:
-                self._encode_int(x)
-            elif PyLong_CheckExact(x):
-                self._encode_long(x)
-            elif PyList_CheckExact(x) or PyTuple_CheckExact(x):
-                self._encode_list(x)
-            elif PyDict_CheckExact(x):
-                self._encode_dict(x)
-            elif PyBool_Check(x):
-                self._encode_int(int(x))
-            elif isinstance(x, Bencached):
-                self._append_string(x.bencoded)
-            else:
-                raise TypeError('unsupported type %r' % x)
-        finally:
-            Py_LeaveRecursiveCall()
-
-
-def bencode(x):
-    """Encode Python object x to string"""
-    encoder = Encoder()
-    encoder.process(x)
-    return encoder.to_bytes()
diff -pruN 0.2-1/fastbencode/python-compat.h 0.3.7-1/fastbencode/python-compat.h
--- 0.2-1/fastbencode/python-compat.h	2022-09-24 23:00:54.000000000 +0000
+++ 0.3.7-1/fastbencode/python-compat.h	1970-01-01 00:00:00.000000000 +0000
@@ -1,23 +0,0 @@
-/*
- *  Bazaar -- distributed version control
- *
- * Copyright (C) 2008 by Canonical Ltd
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
-
-#ifndef BrzPy_EnterRecursiveCall
-#define BrzPy_EnterRecursiveCall(where) (Py_EnterRecursiveCall(where) == 0)
-#endif
diff -pruN 0.2-1/fastbencode/tests/__init__.py 0.3.7-1/fastbencode/tests/__init__.py
--- 0.2-1/fastbencode/tests/__init__.py	2021-08-18 22:20:19.000000000 +0000
+++ 0.3.7-1/fastbencode/tests/__init__.py	1970-01-01 00:00:00.000000000 +0000
@@ -1,33 +0,0 @@
-# Copyright (C) 2007, 2009, 2010 Canonical Ltd
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-#
-
-"""Tests for fastbencode."""
-
-import unittest
-
-
-def test_suite():
-    names = [
-        'test_bencode',
-        ]
-    module_names = ['fastbencode.tests.' + name for name in names]
-    result = unittest.TestSuite()
-    loader = unittest.TestLoader()
-    suite = loader.loadTestsFromNames(module_names)
-    result.addTests(suite)
-
-    return result
diff -pruN 0.2-1/fastbencode/tests/test_bencode.py 0.3.7-1/fastbencode/tests/test_bencode.py
--- 0.2-1/fastbencode/tests/test_bencode.py	2023-02-11 13:50:44.000000000 +0000
+++ 0.3.7-1/fastbencode/tests/test_bencode.py	1970-01-01 00:00:00.000000000 +0000
@@ -1,413 +0,0 @@
-# Copyright (C) 2007, 2009, 2010, 2016 Canonical Ltd
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-"""Tests for bencode structured encoding"""
-
-import copy
-import sys
-
-from unittest import TestCase, TestSuite
-
-
-def get_named_object(module_name, member_name=None):
-    """Get the Python object named by a given module and member name.
-
-    This is usually much more convenient than dealing with ``__import__``
-    directly::
-
-        >>> doc = get_named_object('pyutils', 'get_named_object.__doc__')
-        >>> doc.splitlines()[0]
-        'Get the Python object named by a given module and member name.'
-
-    :param module_name: a module name, as would be found in sys.modules if
-        the module is already imported.  It may contain dots.  e.g. 'sys' or
-        'os.path'.
-    :param member_name: (optional) a name of an attribute in that module to
-        return.  It may contain dots.  e.g. 'MyClass.some_method'.  If not
-        given, the named module will be returned instead.
-    :raises: ImportError or AttributeError.
-    """
-    # We may have just a module name, or a module name and a member name,
-    # and either may contain dots.  __import__'s return value is a bit
-    # unintuitive, so we need to take care to always return the object
-    # specified by the full combination of module name + member name.
-    if member_name:
-        # Give __import__ a from_list.  It will return the last module in
-        # the dotted module name.
-        attr_chain = member_name.split('.')
-        from_list = attr_chain[:1]
-        obj = __import__(module_name, {}, {}, from_list)
-        for attr in attr_chain:
-            obj = getattr(obj, attr)
-    else:
-        # We're just importing a module, no attributes, so we have no
-        # from_list.  __import__ will return the first module in the dotted
-        # module name, so we look up the module from sys.modules.
-        __import__(module_name, globals(), locals(), [])
-        obj = sys.modules[module_name]
-    return obj
-
-
-def iter_suite_tests(suite):
-    """Return all tests in a suite, recursing through nested suites"""
-    if isinstance(suite, TestCase):
-        yield suite
-    elif isinstance(suite, TestSuite):
-        for item in suite:
-            yield from iter_suite_tests(item)
-    else:
-        raise Exception('unknown type %r for object %r'
-                        % (type(suite), suite))
-
-
-def clone_test(test, new_id):
-    """Clone a test giving it a new id.
-
-    :param test: The test to clone.
-    :param new_id: The id to assign to it.
-    :return: The new test.
-    """
-    new_test = copy.copy(test)
-    new_test.id = lambda: new_id
-    # XXX: Workaround <https://bugs.launchpad.net/testtools/+bug/637725>, which
-    # causes cloned tests to share the 'details' dict.  This makes it hard to
-    # read the test output for parameterized tests, because tracebacks will be
-    # associated with irrelevant tests.
-    try:
-        new_test._TestCase__details
-    except AttributeError:
-        # must be a different version of testtools than expected.  Do nothing.
-        pass
-    else:
-        # Reset the '__details' dict.
-        new_test._TestCase__details = {}
-    return new_test
-
-
-def apply_scenario(test, scenario):
-    """Copy test and apply scenario to it.
-
-    :param test: A test to adapt.
-    :param scenario: A tuple describing the scenario.
-        The first element of the tuple is the new test id.
-        The second element is a dict containing attributes to set on the
-        test.
-    :return: The adapted test.
-    """
-    new_id = "{}({})".format(test.id(), scenario[0])
-    new_test = clone_test(test, new_id)
-    for name, value in scenario[1].items():
-        setattr(new_test, name, value)
-    return new_test
-
-
-def apply_scenarios(test, scenarios, result):
-    """Apply the scenarios in scenarios to test and add to result.
-
-    :param test: The test to apply scenarios to.
-    :param scenarios: An iterable of scenarios to apply to test.
-    :return: result
-    :seealso: apply_scenario
-    """
-    for scenario in scenarios:
-        result.addTest(apply_scenario(test, scenario))
-    return result
-
-
-def multiply_tests(tests, scenarios, result):
-    """Multiply tests_list by scenarios into result.
-
-    This is the core workhorse for test parameterisation.
-
-    Typically the load_tests() method for a per-implementation test suite will
-    call multiply_tests and return the result.
-
-    :param tests: The tests to parameterise.
-    :param scenarios: The scenarios to apply: pairs of (scenario_name,
-        scenario_param_dict).
-    :param result: A TestSuite to add created tests to.
-
-    This returns the passed in result TestSuite with the cross product of all
-    the tests repeated once for each scenario.  Each test is adapted by adding
-    the scenario name at the end of its id(), and updating the test object's
-    __dict__ with the scenario_param_dict.
-
-    >>> import tests.test_sampler
-    >>> r = multiply_tests(
-    ...     tests.test_sampler.DemoTest('test_nothing'),
-    ...     [('one', dict(param=1)),
-    ...      ('two', dict(param=2))],
-    ...     TestUtil.TestSuite())
-    >>> tests = list(iter_suite_tests(r))
-    >>> len(tests)
-    2
-    >>> tests[0].id()
-    'tests.test_sampler.DemoTest.test_nothing(one)'
-    >>> tests[0].param
-    1
-    >>> tests[1].param
-    2
-    """
-    for test in iter_suite_tests(tests):
-        apply_scenarios(test, scenarios, result)
-    return result
-
-
-def permute_tests_for_extension(standard_tests, loader, py_module_name,
-                                ext_module_name):
-    """Helper for permutating tests against an extension module.
-
-    This is meant to be used inside a modules 'load_tests()' function. It will
-    create 2 scenarios, and cause all tests in the 'standard_tests' to be run
-    against both implementations. Setting 'test.module' to the appropriate
-    module. See tests.test__chk_map.load_tests as an example.
-
-    :param standard_tests: A test suite to permute
-    :param loader: A TestLoader
-    :param py_module_name: The python path to a python module that can always
-        be loaded, and will be considered the 'python' implementation. (eg
-        '_chk_map_py')
-    :param ext_module_name: The python path to an extension module. If the
-        module cannot be loaded, a single test will be added, which notes that
-        the module is not available. If it can be loaded, all standard_tests
-        will be run against that module.
-    :return: (suite, feature) suite is a test-suite that has all the permuted
-        tests. feature is the Feature object that can be used to determine if
-        the module is available.
-    """
-
-    py_module = get_named_object(py_module_name)
-    scenarios = [
-        ('python', {'module': py_module}),
-    ]
-    suite = loader.suiteClass()
-    try:
-        __import__(ext_module_name)
-    except ModuleNotFoundError:
-        pass
-    else:
-        scenarios.append(('C', {'module': get_named_object(ext_module_name)}))
-    result = multiply_tests(standard_tests, scenarios, suite)
-    return result
-
-
-def load_tests(loader, standard_tests, pattern):
-    return permute_tests_for_extension(
-        standard_tests, loader, 'fastbencode._bencode_py',
-        'fastbencode._bencode_pyx')
-
-
-class RecursionLimit:
-    """Context manager that lowers recursion limit for testing."""
-
-    def __init__(self, limit=100):
-        self._new_limit = limit
-        self._old_limit = sys.getrecursionlimit()
-
-    def __enter__(self):
-        sys.setrecursionlimit(self._new_limit)
-        return self
-
-    def __exit__(self, *exc_info):
-        sys.setrecursionlimit(self._old_limit)
-
-
-class TestBencodeDecode(TestCase):
-
-    module = None
-
-    def _check(self, expected, source):
-        self.assertEqual(expected, self.module.bdecode(source))
-
-    def _run_check_error(self, exc, bad):
-        """Check that bdecoding a string raises a particular exception."""
-        self.assertRaises(exc, self.module.bdecode, bad)
-
-    def test_int(self):
-        self._check(0, b'i0e')
-        self._check(4, b'i4e')
-        self._check(123456789, b'i123456789e')
-        self._check(-10, b'i-10e')
-        self._check(int('1' * 1000), b'i' + (b'1' * 1000) + b'e')
-
-    def test_long(self):
-        self._check(12345678901234567890, b'i12345678901234567890e')
-        self._check(-12345678901234567890, b'i-12345678901234567890e')
-
-    def test_malformed_int(self):
-        self._run_check_error(ValueError, b'ie')
-        self._run_check_error(ValueError, b'i-e')
-        self._run_check_error(ValueError, b'i-010e')
-        self._run_check_error(ValueError, b'i-0e')
-        self._run_check_error(ValueError, b'i00e')
-        self._run_check_error(ValueError, b'i01e')
-        self._run_check_error(ValueError, b'i-03e')
-        self._run_check_error(ValueError, b'i')
-        self._run_check_error(ValueError, b'i123')
-        self._run_check_error(ValueError, b'i341foo382e')
-
-    def test_string(self):
-        self._check(b'', b'0:')
-        self._check(b'abc', b'3:abc')
-        self._check(b'1234567890', b'10:1234567890')
-
-    def test_large_string(self):
-        self.assertRaises(ValueError, self.module.bdecode, b"2147483639:foo")
-
-    def test_malformed_string(self):
-        self._run_check_error(ValueError, b'10:x')
-        self._run_check_error(ValueError, b'10:')
-        self._run_check_error(ValueError, b'10')
-        self._run_check_error(ValueError, b'01:x')
-        self._run_check_error(ValueError, b'00:')
-        self._run_check_error(ValueError, b'35208734823ljdahflajhdf')
-        self._run_check_error(ValueError, b'432432432432432:foo')
-        self._run_check_error(ValueError, b' 1:x')  # leading whitespace
-        self._run_check_error(ValueError, b'-1:x')  # negative
-        self._run_check_error(ValueError, b'1 x')  # space vs colon
-        self._run_check_error(ValueError, b'1x')  # missing colon
-        self._run_check_error(ValueError, (b'1' * 1000) + b':')
-
-    def test_list(self):
-        self._check([], b'le')
-        self._check([b'', b'', b''], b'l0:0:0:e')
-        self._check([1, 2, 3], b'li1ei2ei3ee')
-        self._check([b'asd', b'xy'], b'l3:asd2:xye')
-        self._check([[b'Alice', b'Bob'], [2, 3]], b'll5:Alice3:Bobeli2ei3eee')
-
-    def test_list_deepnested(self):
-        import platform
-        if platform.python_implementation() == 'PyPy':
-            self.skipTest('recursion not an issue on pypy')
-        with RecursionLimit():
-            self._run_check_error(RuntimeError, (b"l" * 100) + (b"e" * 100))
-
-    def test_malformed_list(self):
-        self._run_check_error(ValueError, b'l')
-        self._run_check_error(ValueError, b'l01:ae')
-        self._run_check_error(ValueError, b'l0:')
-        self._run_check_error(ValueError, b'li1e')
-        self._run_check_error(ValueError, b'l-3:e')
-
-    def test_dict(self):
-        self._check({}, b'de')
-        self._check({b'': 3}, b'd0:i3ee')
-        self._check({b'age': 25, b'eyes': b'blue'}, b'd3:agei25e4:eyes4:bluee')
-        self._check({b'spam.mp3': {b'author': b'Alice', b'length': 100000}},
-                    b'd8:spam.mp3d6:author5:Alice6:lengthi100000eee')
-
-    def test_dict_deepnested(self):
-        with RecursionLimit():
-            self._run_check_error(
-                RuntimeError, (b"d0:" * 1000) + b'i1e' + (b"e" * 1000))
-
-    def test_malformed_dict(self):
-        self._run_check_error(ValueError, b'd')
-        self._run_check_error(ValueError, b'defoobar')
-        self._run_check_error(ValueError, b'd3:fooe')
-        self._run_check_error(ValueError, b'di1e0:e')
-        self._run_check_error(ValueError, b'd1:b0:1:a0:e')
-        self._run_check_error(ValueError, b'd1:a0:1:a0:e')
-        self._run_check_error(ValueError, b'd0:0:')
-        self._run_check_error(ValueError, b'd0:')
-        self._run_check_error(ValueError, b'd432432432432432432:e')
-
-    def test_empty_string(self):
-        self.assertRaises(ValueError, self.module.bdecode, b'')
-
-    def test_junk(self):
-        self._run_check_error(ValueError, b'i6easd')
-        self._run_check_error(ValueError, b'2:abfdjslhfld')
-        self._run_check_error(ValueError, b'0:0:')
-        self._run_check_error(ValueError, b'leanfdldjfh')
-
-    def test_unknown_object(self):
-        self.assertRaises(ValueError, self.module.bdecode, b'relwjhrlewjh')
-
-    def test_unsupported_type(self):
-        self._run_check_error(TypeError, float(1.5))
-        self._run_check_error(TypeError, None)
-        self._run_check_error(TypeError, lambda x: x)
-        self._run_check_error(TypeError, object)
-        self._run_check_error(TypeError, "ie")
-
-    def test_decoder_type_error(self):
-        self.assertRaises(TypeError, self.module.bdecode, 1)
-
-
-class TestBencodeEncode(TestCase):
-
-    module = None
-
-    def _check(self, expected, source):
-        self.assertEqual(expected, self.module.bencode(source))
-
-    def test_int(self):
-        self._check(b'i4e', 4)
-        self._check(b'i0e', 0)
-        self._check(b'i-10e', -10)
-
-    def test_long(self):
-        self._check(b'i12345678901234567890e', 12345678901234567890)
-        self._check(b'i-12345678901234567890e', -12345678901234567890)
-
-    def test_string(self):
-        self._check(b'0:', b'')
-        self._check(b'3:abc', b'abc')
-        self._check(b'10:1234567890', b'1234567890')
-
-    def test_list(self):
-        self._check(b'le', [])
-        self._check(b'li1ei2ei3ee', [1, 2, 3])
-        self._check(b'll5:Alice3:Bobeli2ei3eee', [[b'Alice', b'Bob'], [2, 3]])
-
-    def test_list_as_tuple(self):
-        self._check(b'le', ())
-        self._check(b'li1ei2ei3ee', (1, 2, 3))
-        self._check(b'll5:Alice3:Bobeli2ei3eee', ((b'Alice', b'Bob'), (2, 3)))
-
-    def test_list_deep_nested(self):
-        top = []
-        lst = top
-        for unused_i in range(1000):
-            lst.append([])
-            lst = lst[0]
-        with RecursionLimit():
-            self.assertRaises(RuntimeError, self.module.bencode, top)
-
-    def test_dict(self):
-        self._check(b'de', {})
-        self._check(b'd3:agei25e4:eyes4:bluee', {b'age': 25, b'eyes': b'blue'})
-        self._check(b'd8:spam.mp3d6:author5:Alice6:lengthi100000eee',
-                    {b'spam.mp3': {b'author': b'Alice', b'length': 100000}})
-
-    def test_dict_deep_nested(self):
-        d = top = {}
-        for i in range(1000):
-            d[b''] = {}
-            d = d[b'']
-        with RecursionLimit():
-            self.assertRaises(RuntimeError, self.module.bencode, top)
-
-    def test_bencached(self):
-        self._check(b'i3e', self.module.Bencached(self.module.bencode(3)))
-
-    def test_invalid_dict(self):
-        self.assertRaises(TypeError, self.module.bencode, {1: b"foo"})
-
-    def test_bool(self):
-        self._check(b'i1e', True)
-        self._check(b'i0e', False)
diff -pruN 0.2-1/fastbencode.egg-info/PKG-INFO 0.3.7-1/fastbencode.egg-info/PKG-INFO
--- 0.2-1/fastbencode.egg-info/PKG-INFO	2023-02-11 13:50:44.000000000 +0000
+++ 0.3.7-1/fastbencode.egg-info/PKG-INFO	1970-01-01 00:00:00.000000000 +0000
@@ -1,50 +0,0 @@
-Metadata-Version: 2.1
-Name: fastbencode
-Version: 0.2
-Summary: Implementation of bencode with optional fast C extensions
-Home-page: https://github.com/breezy-team/fastbencode
-Maintainer: Breezy Developers
-Maintainer-email: breezy-core@googlegroups.com
-License: GPLv2 or later
-Project-URL: GitHub, https://github.com/breezy-team/fastbencode
-Classifier: Programming Language :: Python :: 3.7
-Classifier: Programming Language :: Python :: 3.8
-Classifier: Programming Language :: Python :: 3.9
-Classifier: Programming Language :: Python :: 3.10
-Classifier: Programming Language :: Python :: 3.11
-Classifier: Programming Language :: Python :: Implementation :: CPython
-Classifier: Programming Language :: Python :: Implementation :: PyPy
-Classifier: Operating System :: POSIX
-Classifier: Operating System :: Microsoft :: Windows
-Requires-Python: >=3.7
-Provides-Extra: cext
-License-File: COPYING
-
-fastbencode
-===========
-
-fastbencode is an implementation of the bencode serialization format originally
-used by BitTorrent.
-
-The package includes both a pure-Python version and an optional C extension
-based on Cython.  Both provide the same functionality, but the C extension
-provides significantly better performance.
-
-Example:
-
-    >>> from fastbencode import bencode, bdecode
-    >>> bencode([1, 2, b'a', {b'd': 3}])
-    b'li1ei2e1:ad1:di3eee'
-    >>> bdecode(bencode([1, 2, b'a', {b'd': 3}]))
-    [1, 2, b'a', {b'd': 3}]
-
-License
-=======
-fastbencode is available under the GNU GPL, version 2 or later.
-
-Copyright
-=========
-
-* Original Pure-Python bencoder (c) Petru Paler
-* Cython version and modifications (c) Canonical Ltd
-* Split out from Bazaar/Breezy by Jelmer Vernooĳ
diff -pruN 0.2-1/fastbencode.egg-info/SOURCES.txt 0.3.7-1/fastbencode.egg-info/SOURCES.txt
--- 0.2-1/fastbencode.egg-info/SOURCES.txt	2023-02-11 13:50:44.000000000 +0000
+++ 0.3.7-1/fastbencode.egg-info/SOURCES.txt	1970-01-01 00:00:00.000000000 +0000
@@ -1,27 +0,0 @@
-.gitignore
-CODE_OF_CONDUCT.md
-COPYING
-MANIFEST.in
-README.md
-SECURITY.md
-disperse.conf
-pyproject.toml
-setup.cfg
-setup.py
-.github/workflows/disperse.yml
-.github/workflows/pythonpackage.yml
-.github/workflows/pythonpublish.yml
-fastbencode/__init__.py
-fastbencode/_bencode_py.py
-fastbencode/_bencode_pyx.h
-fastbencode/_bencode_pyx.pyi
-fastbencode/_bencode_pyx.pyx
-fastbencode/py.typed
-fastbencode/python-compat.h
-fastbencode.egg-info/PKG-INFO
-fastbencode.egg-info/SOURCES.txt
-fastbencode.egg-info/dependency_links.txt
-fastbencode.egg-info/requires.txt
-fastbencode.egg-info/top_level.txt
-fastbencode/tests/__init__.py
-fastbencode/tests/test_bencode.py
\ No newline at end of file
diff -pruN 0.2-1/fastbencode.egg-info/dependency_links.txt 0.3.7-1/fastbencode.egg-info/dependency_links.txt
--- 0.2-1/fastbencode.egg-info/dependency_links.txt	2023-02-11 13:50:44.000000000 +0000
+++ 0.3.7-1/fastbencode.egg-info/dependency_links.txt	1970-01-01 00:00:00.000000000 +0000
@@ -1 +0,0 @@
-
diff -pruN 0.2-1/fastbencode.egg-info/requires.txt 0.3.7-1/fastbencode.egg-info/requires.txt
--- 0.2-1/fastbencode.egg-info/requires.txt	2023-02-11 13:50:44.000000000 +0000
+++ 0.3.7-1/fastbencode.egg-info/requires.txt	1970-01-01 00:00:00.000000000 +0000
@@ -1,3 +0,0 @@
-
-[cext]
-cython>=0.29
diff -pruN 0.2-1/fastbencode.egg-info/top_level.txt 0.3.7-1/fastbencode.egg-info/top_level.txt
--- 0.2-1/fastbencode.egg-info/top_level.txt	2023-02-11 13:50:44.000000000 +0000
+++ 0.3.7-1/fastbencode.egg-info/top_level.txt	1970-01-01 00:00:00.000000000 +0000
@@ -1 +0,0 @@
-fastbencode
diff -pruN 0.2-1/pyproject.toml 0.3.7-1/pyproject.toml
--- 0.2-1/pyproject.toml	2022-10-19 08:31:10.000000000 +0000
+++ 0.3.7-1/pyproject.toml	2025-11-04 15:53:48.000000000 +0000
@@ -1,3 +1,95 @@
 [build-system]
-requires = ["setuptools", "cython>=0.29", "packaging"]
+requires = [
+    "setuptools>=61.2",
+    "setuptools-rust>=1.0.0",
+]
 build-backend = "setuptools.build_meta"
+
+[project]
+name = "fastbencode"
+description = "Implementation of bencode with optional fast Rust extensions"
+maintainers = [{name = "Breezy Developers", email = "breezy-core@googlegroups.com"}]
+readme = "README.md"
+license = "Apache-2.0"
+classifiers = [
+    "Programming Language :: Python :: 3.10",
+    "Programming Language :: Python :: 3.11",
+    "Programming Language :: Python :: 3.12",
+    "Programming Language :: Python :: 3.13",
+    "Programming Language :: Python :: 3.14",
+    "Programming Language :: Python :: Implementation :: CPython",
+    "Programming Language :: Python :: Implementation :: PyPy",
+    "Operating System :: POSIX",
+    "Operating System :: Microsoft :: Windows",
+]
+requires-python = ">=3.10"
+dynamic = ["version"]
+dependencies = []
+
+[project.urls]
+Homepage = "https://github.com/breezy-team/fastbencode"
+GitHub = "https://github.com/breezy-team/fastbencode"
+
+[project.optional-dependencies]
+rust = ["setuptools-rust>=1.0.0"]
+dev = [
+    "ruff==0.14.3"
+]
+
+[tool.setuptools]
+packages = ["fastbencode"]
+include-package-data = false
+
+[tool.setuptools.dynamic]
+version = {attr = "fastbencode.__version__"}
+
+[tool.ruff]
+target-version = "py37"
+line-length = 79
+
+[tool.ruff.lint]
+select = [
+    "ANN",
+    "D",
+    "E",
+    "F",
+    "I",
+    "UP",
+]
+ignore = [
+    "ANN001",
+    "ANN002",
+    "ANN201",
+    "ANN202",
+    "ANN204",
+    "D100",
+    "D101",
+    "D102",
+    "D103",
+    "D105",
+    "D107",
+]
+
+[tool.ruff.lint.pydocstyle]
+convention = "google"
+
+[tool.cibuildwheel]
+environment = {PATH="$HOME/.cargo/bin:$PATH"}
+before-build = "pip install -U setuptools-rust && curl https://sh.rustup.rs -sSf | sh -s -- --profile=minimal -y && rustup show"
+
+[tool.cibuildwheel.linux]
+skip = "*-musllinux_* *cp314*"
+archs = ["auto", "aarch64"]
+before-build = "pip install -U setuptools-rust && yum -y install libatomic && curl https://sh.rustup.rs -sSf | sh -s -- --profile=minimal -y && rustup show"
+
+[tool.cibuildwheel.macos]
+archs = ["auto", "universal2", "x86_64", "arm64"]
+before-all = "rustup target add x86_64-apple-darwin aarch64-apple-darwin"
+skip = """\
+    cp39-macosx_x86_64 cp39-macosx_universal2 \
+    cp310-macosx_x86_64 cp310-macosx_universal2 \
+    cp311-macosx_x86_64 cp311-macosx_universal2 \
+    cp312-macosx_x86_64 cp312-macosx_universal2 \
+    cp313-macosx_x86_64 cp313-macosx_universal2 \
+    cp314-macosx_x86_64 cp314-macosx_universal2 \
+    """
diff -pruN 0.2-1/setup.cfg 0.3.7-1/setup.cfg
--- 0.2-1/setup.cfg	2023-02-11 13:50:44.000000000 +0000
+++ 0.3.7-1/setup.cfg	1970-01-01 00:00:00.000000000 +0000
@@ -1,35 +0,0 @@
-[metadata]
-name = fastbencode
-description = Implementation of bencode with optional fast C extensions
-maintainer = Breezy Developers
-maintainer_email = breezy-core@googlegroups.com
-url = https://github.com/breezy-team/fastbencode
-long_description = file:README.md
-license = GPLv2 or later
-version = attr:fastbencode.__version__
-project_urls = 
-	GitHub = https://github.com/breezy-team/fastbencode
-classifiers = 
-	Programming Language :: Python :: 3.7
-	Programming Language :: Python :: 3.8
-	Programming Language :: Python :: 3.9
-	Programming Language :: Python :: 3.10
-	Programming Language :: Python :: 3.11
-	Programming Language :: Python :: Implementation :: CPython
-	Programming Language :: Python :: Implementation :: PyPy
-	Operating System :: POSIX
-	Operating System :: Microsoft :: Windows
-
-[options]
-python_requires = >=3.7
-packages = fastbencode
-setup_requires = 
-	cython>=0.29
-
-[options.extras_require]
-cext = cython>=0.29
-
-[egg_info]
-tag_build = 
-tag_date = 0
-
diff -pruN 0.2-1/setup.py 0.3.7-1/setup.py
--- 0.2-1/setup.py	2023-02-11 13:50:44.000000000 +0000
+++ 0.3.7-1/setup.py	2025-11-04 15:53:48.000000000 +0000
@@ -1,82 +1,16 @@
 #!/usr/bin/python3
 
-import os
-import sys
-from setuptools import setup, Extension
-try:
-    from packaging.version import Version
-except ImportError:
-    from distutils.version import LooseVersion as Version
 
-try:
-    from Cython.Distutils import build_ext
-    from Cython.Compiler.Version import version as cython_version
-except ImportError:
-    have_cython = False
-    # try to build the extension from the prior generated source.
-    print("")
-    print("The python package 'Cython' is not available."
-          " If the .c files are available,")
-    print("they will be built,"
-          " but modifying the .pyx files will not rebuild them.")
-    print("")
-    from distutils.command.build_ext import build_ext
-else:
-    minimum_cython_version = '0.29'
-    cython_version_info = Version(cython_version)
-    if cython_version_info < Version(minimum_cython_version):
-        print("Version of Cython is too old. "
-              "Current is %s, need at least %s."
-              % (cython_version, minimum_cython_version))
-        print("If the .c files are available, they will be built,"
-              " but modifying the .pyx files will not rebuild them.")
-        have_cython = False
-    else:
-        have_cython = True
+from setuptools import setup
+from setuptools_rust import Binding, RustExtension
 
-
-ext_modules = []
-
-
-def add_cython_extension(module_name, libraries=None, extra_source=[]):
-    """Add a cython module to build.
-
-    This will use Cython to auto-generate the .c file if it is available.
-    Otherwise it will fall back on the .c file. If the .c file is not
-    available, it will warn, and not add anything.
-
-    You can pass any extra options to Extension through kwargs. One example is
-    'libraries = []'.
-
-    :param module_name: The python path to the module. This will be used to
-        determine the .pyx and .c files to use.
-    """
-    path = module_name.replace('.', '/')
-    cython_name = path + '.pyx'
-    c_name = path + '.c'
-    define_macros = []
-    if sys.platform == 'win32':
-        # cython uses the macro WIN32 to detect the platform, even though it
-        # should be using something like _WIN32 or MS_WINDOWS, oh well, we can
-        # give it the right value.
-        define_macros.append(('WIN32', None))
-    if have_cython:
-        source = [cython_name]
-    else:
-        if not os.path.isfile(c_name):
-            return
-        else:
-            source = [c_name]
-    source.extend(extra_source)
-    include_dirs = ['fastbencode']
-    ext_modules.append(
-        Extension(
-            module_name, source, define_macros=define_macros,
-            libraries=libraries, include_dirs=include_dirs,
-            optional=os.environ.get('CIBUILDWHEEL', '0') != '1'))
-
-
-add_cython_extension('fastbencode._bencode_pyx')
-
-
-setup(ext_modules=ext_modules, cmdclass={'build_ext': build_ext})
+setup(
+    rust_extensions=[
+        RustExtension(
+            "fastbencode._bencode_rs",
+            binding=Binding.PyO3,
+            py_limited_api=False,
+            optional=True,
+        )
+    ],
+)
diff -pruN 0.2-1/src/lib.rs 0.3.7-1/src/lib.rs
--- 0.2-1/src/lib.rs	1970-01-01 00:00:00.000000000 +0000
+++ 0.3.7-1/src/lib.rs	2025-11-04 15:53:48.000000000 +0000
@@ -0,0 +1,438 @@
+#![allow(non_snake_case)]
+use pyo3::exceptions::{PyTypeError, PyValueError};
+use pyo3::prelude::*;
+use pyo3::types::{PyBytes, PyDict, PyInt, PyList, PyString, PyTuple};
+
+#[pyclass]
+struct Bencached {
+    #[pyo3(get)]
+    bencoded: Py<PyBytes>,
+}
+
+#[pymethods]
+impl Bencached {
+    #[new]
+    fn new(s: Py<PyBytes>) -> Self {
+        Bencached { bencoded: s }
+    }
+
+    fn as_bytes(&self, py: Python) -> PyResult<&[u8]> {
+        Ok(self.bencoded.as_bytes(py))
+    }
+}
+
+#[pyclass]
+struct Decoder {
+    data: Vec<u8>,
+    position: usize,
+    yield_tuples: bool,
+    bytestring_encoding: Option<String>,
+}
+
+#[pymethods]
+impl Decoder {
+    #[new]
+    fn new(
+        s: &Bound<PyBytes>,
+        yield_tuples: Option<bool>,
+        bytestring_encoding: Option<String>,
+    ) -> Self {
+        Decoder {
+            data: s.as_bytes().to_vec(),
+            position: 0,
+            yield_tuples: yield_tuples.unwrap_or(false),
+            bytestring_encoding,
+        }
+    }
+
+    fn decode<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
+        let result = self.decode_object(py)?;
+        if self.position < self.data.len() {
+            return Err(PyValueError::new_err("junk in stream"));
+        }
+        Ok(result)
+    }
+
+    fn decode_object<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
+        if self.position >= self.data.len() {
+            return Err(PyValueError::new_err("stream underflow"));
+        }
+
+        // Check for recursion - in a real implementation we would track recursion depth
+        let next_byte = self.data[self.position];
+
+        match next_byte {
+            b'0'..=b'9' => Ok(self.decode_bytes(py)?.into_any()),
+            b'l' => {
+                self.position += 1;
+                Ok(self.decode_list(py)?.into_any())
+            }
+            b'i' => {
+                self.position += 1;
+                Ok(self.decode_int(py)?.into_any())
+            }
+            b'd' => {
+                self.position += 1;
+                Ok(self.decode_dict(py)?.into_any())
+            }
+            _ => Err(PyValueError::new_err(format!(
+                "unknown object type identifier {:?}",
+                next_byte as char
+            ))),
+        }
+    }
+
+    fn read_digits(&mut self, stop_char: u8) -> PyResult<String> {
+        let start = self.position;
+        while self.position < self.data.len() {
+            let b = self.data[self.position];
+            if b == stop_char {
+                break;
+            }
+            if (b < b'0' || b > b'9') && b != b'-' {
+                return Err(PyValueError::new_err(format!(
+                    "Stop character {} not found: {}",
+                    stop_char as char, b as char
+                )));
+            }
+            self.position += 1;
+        }
+
+        if self.position >= self.data.len() || self.data[self.position] != stop_char {
+            return Err(PyValueError::new_err(format!(
+                "Stop character {} not found",
+                stop_char as char
+            )));
+        }
+
+        // Check for leading zeros
+        if self.data[start] == b'0' && self.position - start > 1 {
+            return Err(PyValueError::new_err("leading zeros are not allowed"));
+        } else if self.data[start] == b'-'
+            && self.data[start + 1] == b'0'
+            && self.position - start > 2
+        {
+            return Err(PyValueError::new_err("leading zeros are not allowed"));
+        }
+
+        Ok(String::from_utf8_lossy(&self.data[start..self.position]).to_string())
+    }
+
+    fn decode_int<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
+        let digits = self.read_digits(b'e')?;
+
+        // Move past the 'e'
+        self.position += 1;
+
+        // Check for negative zero
+        if digits == "-0" {
+            return Err(PyValueError::new_err("negative zero not allowed"));
+        }
+
+        // Parse the integer directly
+        let parsed_int = match digits.parse::<i64>() {
+            Ok(n) => n.into_pyobject(py)?.into_any(),
+            Err(_) => {
+                // For very large integers, fallback to Python's conversion
+                let py_str = PyString::new(py, &digits);
+
+                let int_type = py.get_type::<PyInt>();
+                int_type.call1((py_str,))?
+            }
+        };
+
+        Ok(parsed_int.into_any())
+    }
+
+    fn decode_bytes<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
+        let len_end_pos = self.data[self.position..].iter().position(|&b| b == b':');
+        if len_end_pos.is_none() {
+            return Err(PyValueError::new_err("string len not terminated by \":\""));
+        }
+
+        let len_end_pos = len_end_pos.unwrap() + self.position;
+        let len_str = std::str::from_utf8(&self.data[self.position..len_end_pos])
+            .map_err(|_| PyValueError::new_err("invalid length string"))?;
+
+        // Check for leading zeros in the length
+        if len_str.starts_with('0') && len_str.len() > 1 {
+            return Err(PyValueError::new_err("leading zeros are not allowed"));
+        }
+
+        let length: usize = len_str
+            .parse()
+            .map_err(|_| PyValueError::new_err("invalid length value"))?;
+
+        // Skip past the ':' character
+        self.position = len_end_pos + 1;
+
+        if length > self.data.len() - self.position {
+            return Err(PyValueError::new_err("stream underflow"));
+        }
+
+        let bytes_slice = &self.data[self.position..self.position + length];
+        self.position += length;
+
+        let bytes_obj = PyBytes::new(py, bytes_slice).into_any();
+
+        // Return as bytes or decode depending on bytestring_encoding
+        if let Some(encoding) = &self.bytestring_encoding {
+            Ok(PyString::from_object(&bytes_obj, encoding, "strict")?.into_any())
+        } else {
+            Ok(bytes_obj)
+        }
+    }
+
+    fn decode_list<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
+        let mut result = Vec::new();
+
+        while self.position < self.data.len() && self.data[self.position] != b'e' {
+            let item = self.decode_object(py)?;
+            result.push(item);
+        }
+
+        if self.position >= self.data.len() {
+            return Err(PyValueError::new_err("malformed list"));
+        }
+
+        // Skip the 'e'
+        self.position += 1;
+
+        if self.yield_tuples {
+            let tuple = PyTuple::new(py, &result)?;
+            Ok(tuple.into_any())
+        } else {
+            let list = PyList::new(py, &result)?;
+            Ok(list.into_any())
+        }
+    }
+
+    fn decode_dict<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
+        let dict = PyDict::new(py);
+        let mut last_key: Option<Vec<u8>> = None;
+
+        while self.position < self.data.len() && self.data[self.position] != b'e' {
+            // Keys should be strings only
+            if self.data[self.position] < b'0' || self.data[self.position] > b'9' {
+                return Err(PyValueError::new_err("key was not a simple string"));
+            }
+
+            // Decode key as bytes
+            let key_obj = self.decode_bytes(py)?;
+
+            // Get bytes representation for comparison
+            let key_bytes = if let Some(encoding) = &self.bytestring_encoding {
+                if encoding == "utf-8" {
+                    let key_str = key_obj.extract::<&str>()?;
+                    key_str.as_bytes().to_vec()
+                } else {
+                    let key_bytes = key_obj.extract::<Bound<PyBytes>>()?;
+                    key_bytes.as_bytes().to_vec()
+                }
+            } else {
+                let key_bytes = key_obj.extract::<Bound<PyBytes>>()?;
+                key_bytes.as_bytes().to_vec()
+            };
+
+            // Check key ordering
+            if let Some(ref last) = last_key {
+                if last >= &key_bytes {
+                    return Err(PyValueError::new_err("dict keys disordered"));
+                }
+            }
+
+            last_key = Some(key_bytes);
+
+            // Decode value
+            let value = self.decode_object(py)?;
+
+            // Insert into dictionary
+            dict.set_item(key_obj, value)?;
+        }
+
+        if self.position >= self.data.len() {
+            return Err(PyValueError::new_err("malformed dict"));
+        }
+
+        // Skip the 'e'
+        self.position += 1;
+
+        Ok(dict)
+    }
+}
+
+#[pyclass]
+struct Encoder {
+    buffer: Vec<u8>,
+    bytestring_encoding: Option<String>,
+}
+
+#[pymethods]
+impl Encoder {
+    #[new]
+    fn new(_maxsize: Option<usize>, bytestring_encoding: Option<String>) -> Self {
+        Encoder {
+            buffer: Vec::new(),
+            bytestring_encoding,
+        }
+    }
+
+    fn to_bytes<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> {
+        PyBytes::new(py, &self.buffer)
+    }
+
+    fn process(&mut self, py: Python, x: Bound<PyAny>) -> PyResult<()> {
+        if let Ok(s) = x.extract::<Bound<PyBytes>>() {
+            self.encode_bytes(s)?;
+        } else if let Ok(n) = x.extract::<i64>() {
+            self.encode_int(n)?;
+        } else if let Ok(n) = x.extract::<Bound<PyInt>>() {
+            self.encode_long(n)?;
+        } else if x.is_instance_of::<PyList>() {
+            self.encode_list(py, x)?;
+        } else if x.is_instance_of::<PyTuple>() {
+            self.encode_list(py, x)?;
+        } else if let Ok(d) = x.extract::<Bound<PyDict>>() {
+            self.encode_dict(py, d)?;
+        } else if let Ok(b) = x.extract::<bool>() {
+            self.encode_int(if b { 1 } else { 0 })?;
+        } else if let Ok(obj) = x.extract::<PyRef<Bencached>>() {
+            self.append_bytes(obj.as_bytes(py)?)?;
+        } else if let Ok(s) = x.extract::<&str>() {
+            self.encode_string(s)?;
+        } else {
+            return Err(PyTypeError::new_err(format!("unsupported type: {:?}", x)));
+        }
+        Ok(())
+    }
+
+    fn encode_int(&mut self, x: i64) -> PyResult<()> {
+        let s = format!("i{}e", x);
+        self.buffer.extend(s.as_bytes());
+        Ok(())
+    }
+
+    fn encode_long(&mut self, x: Bound<PyInt>) -> PyResult<()> {
+        let s = format!("i{}e", x.str()?);
+        self.buffer.extend(s.as_bytes());
+        Ok(())
+    }
+
+    fn append_bytes(&mut self, bytes: &[u8]) -> PyResult<()> {
+        self.buffer.extend(bytes);
+        Ok(())
+    }
+
+    fn encode_bytes(&mut self, bytes: Bound<PyBytes>) -> PyResult<()> {
+        let len_str = format!("{}:", bytes.len()?);
+        self.buffer.extend(len_str.as_bytes());
+        self.buffer.extend(bytes.as_bytes());
+        Ok(())
+    }
+
+    fn encode_string(&mut self, x: &str) -> PyResult<()> {
+        if let Some(encoding) = &self.bytestring_encoding {
+            if encoding == "utf-8" {
+                let len_str = format!("{}:", x.len());
+                self.buffer.extend(len_str.as_bytes());
+                self.buffer.extend(x.as_bytes());
+                Ok(())
+            } else {
+                Err(PyTypeError::new_err(
+                    "Only utf-8 encoding is supported for string encoding",
+                ))
+            }
+        } else {
+            Err(PyTypeError::new_err(
+                "string found but no encoding specified. Use bencode_utf8 rather bencode?",
+            ))
+        }
+    }
+
+    fn encode_list(&mut self, py: Python, sequence: Bound<PyAny>) -> PyResult<()> {
+        self.buffer.push(b'l');
+
+        for item in sequence.try_iter()? {
+            self.process(py, item?.into())?;
+        }
+
+        self.buffer.push(b'e');
+        Ok(())
+    }
+
+    fn encode_dict(&mut self, py: Python, dict: Bound<PyDict>) -> PyResult<()> {
+        self.buffer.push(b'd');
+
+        // Get all keys and sort them
+        let mut keys: Vec<Bound<PyBytes>> = dict
+            .keys()
+            .iter()
+            .map(|key| key.extract::<Bound<PyBytes>>())
+            .collect::<PyResult<Vec<_>>>()?;
+        keys.sort_by(|a, b| {
+            let a_str = a.extract::<&[u8]>().unwrap();
+            let b_str = b.extract::<&[u8]>().unwrap();
+            a_str.cmp(b_str)
+        });
+
+        for key in keys {
+            if let Ok(bytes) = key.extract::<Bound<PyBytes>>() {
+                self.encode_bytes(bytes)?;
+            } else {
+                return Err(PyTypeError::new_err("key in dict should be string"));
+            }
+
+            if let Some(value) = dict.get_item(key)? {
+                self.process(py, value.into())?;
+            }
+        }
+
+        self.buffer.push(b'e');
+        Ok(())
+    }
+}
+
+#[pyfunction]
+fn bdecode<'py>(py: Python<'py>, s: &Bound<PyBytes>) -> PyResult<Bound<'py, PyAny>> {
+    let mut decoder = Decoder::new(s, None, None);
+    decoder.decode(py)
+}
+
+#[pyfunction]
+fn bdecode_as_tuple<'py>(py: Python<'py>, s: &Bound<PyBytes>) -> PyResult<Bound<'py, PyAny>> {
+    let mut decoder = Decoder::new(s, Some(true), None);
+    decoder.decode(py)
+}
+
+#[pyfunction]
+fn bdecode_utf8<'py>(py: Python<'py>, s: &Bound<PyBytes>) -> PyResult<Bound<'py, PyAny>> {
+    let mut decoder = Decoder::new(s, None, Some("utf-8".to_string()));
+    decoder.decode(py)
+}
+
+#[pyfunction]
+fn bencode(py: Python, x: Bound<PyAny>) -> PyResult<Py<PyAny>> {
+    let mut encoder = Encoder::new(None, None);
+    encoder.process(py, x)?;
+    Ok(encoder.to_bytes(py).into())
+}
+
+#[pyfunction]
+fn bencode_utf8(py: Python, x: Bound<PyAny>) -> PyResult<Py<PyAny>> {
+    let mut encoder = Encoder::new(None, Some("utf-8".to_string()));
+    encoder.process(py, x)?;
+    Ok(encoder.to_bytes(py).into())
+}
+
+#[pymodule]
+fn _bencode_rs(m: &Bound<PyModule>) -> PyResult<()> {
+    m.add_class::<Bencached>()?;
+    m.add_class::<Decoder>()?;
+    m.add_class::<Encoder>()?;
+    m.add_function(wrap_pyfunction!(bdecode, m)?)?;
+    m.add_function(wrap_pyfunction!(bdecode_as_tuple, m)?)?;
+    m.add_function(wrap_pyfunction!(bdecode_utf8, m)?)?;
+    m.add_function(wrap_pyfunction!(bencode, m)?)?;
+    m.add_function(wrap_pyfunction!(bencode_utf8, m)?)?;
+    Ok(())
+}
diff -pruN 0.2-1/tests/__init__.py 0.3.7-1/tests/__init__.py
--- 0.2-1/tests/__init__.py	1970-01-01 00:00:00.000000000 +0000
+++ 0.3.7-1/tests/__init__.py	2025-11-04 15:53:48.000000000 +0000
@@ -0,0 +1,33 @@
+# Copyright (C) 2007, 2009, 2010 Canonical Ltd
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+#
+
+"""Tests for fastbencode."""
+
+import unittest
+
+
+def test_suite():
+    names = [
+        "test_bencode",
+    ]
+    module_names = ["tests." + name for name in names]
+    result = unittest.TestSuite()
+    loader = unittest.TestLoader()
+    suite = loader.loadTestsFromNames(module_names)
+    result.addTests(suite)
+
+    return result
diff -pruN 0.2-1/tests/test_bencode.py 0.3.7-1/tests/test_bencode.py
--- 0.2-1/tests/test_bencode.py	1970-01-01 00:00:00.000000000 +0000
+++ 0.3.7-1/tests/test_bencode.py	2025-11-04 15:53:48.000000000 +0000
@@ -0,0 +1,508 @@
+# Copyright (C) 2007, 2009, 2010, 2016 Canonical Ltd
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+"""Tests for bencode structured encoding."""
+
+import copy
+import sys
+from unittest import TestCase, TestSuite
+
+
+def get_named_object(module_name, member_name=None):
+    """Get the Python object named by a given module and member name.
+
+    This is usually much more convenient than dealing with ``__import__``
+    directly::
+
+        >>> doc = get_named_object('pyutils', 'get_named_object.__doc__')
+        >>> doc.splitlines()[0]
+        'Get the Python object named by a given module and member name.'
+
+    :param module_name: a module name, as would be found in sys.modules if
+        the module is already imported.  It may contain dots.  e.g. 'sys' or
+        'os.path'.
+    :param member_name: (optional) a name of an attribute in that module to
+        return.  It may contain dots.  e.g. 'MyClass.some_method'.  If not
+        given, the named module will be returned instead.
+    :raises: ImportError or AttributeError.
+    """
+    # We may have just a module name, or a module name and a member name,
+    # and either may contain dots.  __import__'s return value is a bit
+    # unintuitive, so we need to take care to always return the object
+    # specified by the full combination of module name + member name.
+    if member_name:
+        # Give __import__ a from_list.  It will return the last module in
+        # the dotted module name.
+        attr_chain = member_name.split(".")
+        from_list = attr_chain[:1]
+        obj = __import__(module_name, {}, {}, from_list)
+        for attr in attr_chain:
+            obj = getattr(obj, attr)
+    else:
+        # We're just importing a module, no attributes, so we have no
+        # from_list.  __import__ will return the first module in the dotted
+        # module name, so we look up the module from sys.modules.
+        __import__(module_name, globals(), locals(), [])
+        obj = sys.modules[module_name]
+    return obj
+
+
+def iter_suite_tests(suite):
+    """Return all tests in a suite, recursing through nested suites."""
+    if isinstance(suite, TestCase):
+        yield suite
+    elif isinstance(suite, TestSuite):
+        for item in suite:
+            yield from iter_suite_tests(item)
+    else:
+        raise Exception(f"unknown type {type(suite)!r} for object {suite!r}")
+
+
+def clone_test(test, new_id):
+    """Clone a test giving it a new id.
+
+    :param test: The test to clone.
+    :param new_id: The id to assign to it.
+    :return: The new test.
+    """
+    new_test = copy.copy(test)
+    new_test.id = lambda: new_id
+    # XXX: Workaround <https://bugs.launchpad.net/testtools/+bug/637725>, which
+    # causes cloned tests to share the 'details' dict.  This makes it hard to
+    # read the test output for parameterized tests, because tracebacks will be
+    # associated with irrelevant tests.
+    try:
+        new_test._TestCase__details
+    except AttributeError:
+        # must be a different version of testtools than expected.  Do nothing.
+        pass
+    else:
+        # Reset the '__details' dict.
+        new_test._TestCase__details = {}
+    return new_test
+
+
+def apply_scenario(test, scenario):
+    """Copy test and apply scenario to it.
+
+    :param test: A test to adapt.
+    :param scenario: A tuple describing the scenario.
+        The first element of the tuple is the new test id.
+        The second element is a dict containing attributes to set on the
+        test.
+    :return: The adapted test.
+    """
+    new_id = f"{test.id()}({scenario[0]})"
+    new_test = clone_test(test, new_id)
+    for name, value in scenario[1].items():
+        setattr(new_test, name, value)
+    return new_test
+
+
+def apply_scenarios(test, scenarios, result):
+    """Apply the scenarios in scenarios to test and add to result.
+
+    :param test: The test to apply scenarios to.
+    :param scenarios: An iterable of scenarios to apply to test.
+    :return: result
+    :seealso: apply_scenario
+    """
+    for scenario in scenarios:
+        result.addTest(apply_scenario(test, scenario))
+    return result
+
+
+def multiply_tests(tests, scenarios, result):
+    """Multiply tests_list by scenarios into result.
+
+    This is the core workhorse for test parameterisation.
+
+    Typically the load_tests() method for a per-implementation test suite will
+    call multiply_tests and return the result.
+
+    :param tests: The tests to parameterise.
+    :param scenarios: The scenarios to apply: pairs of (scenario_name,
+        scenario_param_dict).
+    :param result: A TestSuite to add created tests to.
+
+    This returns the passed in result TestSuite with the cross product of all
+    the tests repeated once for each scenario.  Each test is adapted by adding
+    the scenario name at the end of its id(), and updating the test object's
+    __dict__ with the scenario_param_dict.
+
+    >>> import tests.test_sampler
+    >>> r = multiply_tests(
+    ...     tests.test_sampler.DemoTest('test_nothing'),
+    ...     [('one', dict(param=1)),
+    ...      ('two', dict(param=2))],
+    ...     TestUtil.TestSuite())
+    >>> tests = list(iter_suite_tests(r))
+    >>> len(tests)
+    2
+    >>> tests[0].id()
+    'tests.test_sampler.DemoTest.test_nothing(one)'
+    >>> tests[0].param
+    1
+    >>> tests[1].param
+    2
+    """
+    for test in iter_suite_tests(tests):
+        apply_scenarios(test, scenarios, result)
+    return result
+
+
+def permute_tests_for_extension(
+    standard_tests, loader, py_module_name, ext_module_name
+):
+    """Helper for permutating tests against an extension module.
+
+    This is meant to be used inside a modules 'load_tests()' function. It will
+    create 2 scenarios, and cause all tests in the 'standard_tests' to be run
+    against both implementations. Setting 'test.module' to the appropriate
+    module. See tests.test__chk_map.load_tests as an example.
+
+    :param standard_tests: A test suite to permute
+    :param loader: A TestLoader
+    :param py_module_name: The python path to a python module that can always
+        be loaded, and will be considered the 'python' implementation. (eg
+        '_chk_map_py')
+    :param ext_module_name: The python path to an extension module. If the
+        module cannot be loaded, a single test will be added, which notes that
+        the module is not available. If it can be loaded, all standard_tests
+        will be run against that module.
+    :return: (suite, feature) suite is a test-suite that has all the permuted
+        tests. feature is the Feature object that can be used to determine if
+        the module is available.
+    """
+    py_module = get_named_object(py_module_name)
+    scenarios = [
+        ("python", {"module": py_module}),
+    ]
+    suite = loader.suiteClass()
+    try:
+        __import__(ext_module_name)
+    except ModuleNotFoundError:
+        pass
+    else:
+        scenarios.append(("C", {"module": get_named_object(ext_module_name)}))
+    result = multiply_tests(standard_tests, scenarios, suite)
+    return result
+
+
+def load_tests(loader, standard_tests, pattern):
+    return permute_tests_for_extension(
+        standard_tests,
+        loader,
+        "fastbencode._bencode_py",
+        "fastbencode._bencode_rs",
+    )
+
+
+class RecursionLimit:
+    """Context manager that lowers recursion limit for testing."""
+
+    def __init__(self, limit=100) -> None:
+        self._new_limit = limit
+        self._old_limit = sys.getrecursionlimit()
+
+    def __enter__(self):
+        sys.setrecursionlimit(self._new_limit)
+        return self
+
+    def __exit__(self, *exc_info):
+        sys.setrecursionlimit(self._old_limit)
+
+
+class TestBencodeDecode(TestCase):
+    module = None
+
+    def _check(self, expected, source):
+        self.assertEqual(expected, self.module.bdecode(source))
+
+    def _run_check_error(self, exc, bad):
+        """Check that bdecoding a string raises a particular exception."""
+        self.assertRaises(exc, self.module.bdecode, bad)
+
+    def test_int(self):
+        self._check(0, b"i0e")
+        self._check(4, b"i4e")
+        self._check(123456789, b"i123456789e")
+        self._check(-10, b"i-10e")
+        self._check(int("1" * 1000), b"i" + (b"1" * 1000) + b"e")
+
+    def test_long(self):
+        self._check(12345678901234567890, b"i12345678901234567890e")
+        self._check(-12345678901234567890, b"i-12345678901234567890e")
+
+    def test_malformed_int(self):
+        self._run_check_error(ValueError, b"ie")
+        self._run_check_error(ValueError, b"i-e")
+        self._run_check_error(ValueError, b"i-010e")
+        self._run_check_error(ValueError, b"i-0e")
+        self._run_check_error(ValueError, b"i00e")
+        self._run_check_error(ValueError, b"i01e")
+        self._run_check_error(ValueError, b"i-03e")
+        self._run_check_error(ValueError, b"i")
+        self._run_check_error(ValueError, b"i123")
+        self._run_check_error(ValueError, b"i341foo382e")
+
+    def test_string(self):
+        self._check(b"", b"0:")
+        self._check(b"abc", b"3:abc")
+        self._check(b"1234567890", b"10:1234567890")
+
+    def test_large_string(self):
+        self.assertRaises(ValueError, self.module.bdecode, b"2147483639:foo")
+
+    def test_malformed_string(self):
+        self._run_check_error(ValueError, b"10:x")
+        self._run_check_error(ValueError, b"10:")
+        self._run_check_error(ValueError, b"10")
+        self._run_check_error(ValueError, b"01:x")
+        self._run_check_error(ValueError, b"00:")
+        self._run_check_error(ValueError, b"35208734823ljdahflajhdf")
+        self._run_check_error(ValueError, b"432432432432432:foo")
+        self._run_check_error(ValueError, b" 1:x")  # leading whitespace
+        self._run_check_error(ValueError, b"-1:x")  # negative
+        self._run_check_error(ValueError, b"1 x")  # space vs colon
+        self._run_check_error(ValueError, b"1x")  # missing colon
+        self._run_check_error(ValueError, (b"1" * 1000) + b":")
+
+    def test_list(self):
+        self._check([], b"le")
+        self._check([b"", b"", b""], b"l0:0:0:e")
+        self._check([1, 2, 3], b"li1ei2ei3ee")
+        self._check([b"asd", b"xy"], b"l3:asd2:xye")
+        self._check([[b"Alice", b"Bob"], [2, 3]], b"ll5:Alice3:Bobeli2ei3eee")
+
+    def test_list_deepnested(self):
+        import platform
+
+        if (
+            platform.python_implementation() == "PyPy"
+            or sys.version_info[:2] >= (3, 12)
+            or self.id().endswith("(C)")
+        ):
+            expected = []
+            for i in range(99):
+                expected = [expected]
+            self._check(expected, (b"l" * 100) + (b"e" * 100))
+        else:
+            with RecursionLimit():
+                self._run_check_error(
+                    RuntimeError, (b"l" * 100) + (b"e" * 100)
+                )
+
+    def test_malformed_list(self):
+        self._run_check_error(ValueError, b"l")
+        self._run_check_error(ValueError, b"l01:ae")
+        self._run_check_error(ValueError, b"l0:")
+        self._run_check_error(ValueError, b"li1e")
+        self._run_check_error(ValueError, b"l-3:e")
+
+    def test_dict(self):
+        self._check({}, b"de")
+        self._check({b"": 3}, b"d0:i3ee")
+        self._check({b"age": 25, b"eyes": b"blue"}, b"d3:agei25e4:eyes4:bluee")
+        self._check(
+            {b"spam.mp3": {b"author": b"Alice", b"length": 100000}},
+            b"d8:spam.mp3d6:author5:Alice6:lengthi100000eee",
+        )
+
+    def test_dict_deepnested(self):
+        if self.id().endswith("(C)"):
+            self.skipTest("no limit recursion in Rust code")
+
+        with RecursionLimit():
+            self._run_check_error(
+                RuntimeError, (b"d0:" * 1000) + b"i1e" + (b"e" * 1000)
+            )
+
+    def test_malformed_dict(self):
+        self._run_check_error(ValueError, b"d")
+        self._run_check_error(ValueError, b"defoobar")
+        self._run_check_error(ValueError, b"d3:fooe")
+        self._run_check_error(ValueError, b"di1e0:e")
+        self._run_check_error(ValueError, b"d1:b0:1:a0:e")
+        self._run_check_error(ValueError, b"d1:a0:1:a0:e")
+        self._run_check_error(ValueError, b"d0:0:")
+        self._run_check_error(ValueError, b"d0:")
+        self._run_check_error(ValueError, b"d432432432432432432:e")
+
+    def test_empty_string(self):
+        self.assertRaises(ValueError, self.module.bdecode, b"")
+
+    def test_junk(self):
+        self._run_check_error(ValueError, b"i6easd")
+        self._run_check_error(ValueError, b"2:abfdjslhfld")
+        self._run_check_error(ValueError, b"0:0:")
+        self._run_check_error(ValueError, b"leanfdldjfh")
+
+    def test_unknown_object(self):
+        self.assertRaises(ValueError, self.module.bdecode, b"relwjhrlewjh")
+
+    def test_unsupported_type(self):
+        self._run_check_error(TypeError, 1.5)
+        self._run_check_error(TypeError, None)
+        self._run_check_error(TypeError, lambda x: x)
+        self._run_check_error(TypeError, object)
+        self._run_check_error(TypeError, "ie")
+
+    def test_decoder_type_error(self):
+        self.assertRaises(TypeError, self.module.bdecode, 1)
+
+
+class TestBdecodeUtf8(TestCase):
+    module = None
+
+    def _check(self, expected, source):
+        self.assertEqual(expected, self.module.bdecode_utf8(source))
+
+    def _run_check_error(self, exc, bad):
+        """Check that bdecoding a string raises a particular exception."""
+        self.assertRaises(exc, self.module.bdecode_utf8, bad)
+
+    def test_string(self):
+        self._check("", b"0:")
+        self._check("aäc", b"4:a\xc3\xa4c")
+        self._check("1234567890", b"10:1234567890")
+
+    def test_large_string(self):
+        self.assertRaises(
+            ValueError, self.module.bdecode_utf8, b"2147483639:foo"
+        )
+
+    def test_malformed_string(self):
+        self._run_check_error(ValueError, b"10:x")
+        self._run_check_error(ValueError, b"10:")
+        self._run_check_error(ValueError, b"10")
+        self._run_check_error(ValueError, b"01:x")
+        self._run_check_error(ValueError, b"00:")
+        self._run_check_error(ValueError, b"35208734823ljdahflajhdf")
+        self._run_check_error(ValueError, b"432432432432432:foo")
+        self._run_check_error(ValueError, b" 1:x")  # leading whitespace
+        self._run_check_error(ValueError, b"-1:x")  # negative
+        self._run_check_error(ValueError, b"1 x")  # space vs colon
+        self._run_check_error(ValueError, b"1x")  # missing colon
+        self._run_check_error(ValueError, (b"1" * 1000) + b":")
+
+    def test_empty_string(self):
+        self.assertRaises(ValueError, self.module.bdecode_utf8, b"")
+
+    def test_invalid_utf8(self):
+        self._run_check_error(UnicodeDecodeError, b"3:\xff\xfe\xfd")
+
+
+class TestBencodeEncode(TestCase):
+    module = None
+
+    def _check(self, expected, source):
+        self.assertEqual(expected, self.module.bencode(source))
+
+    def test_int(self):
+        self._check(b"i4e", 4)
+        self._check(b"i0e", 0)
+        self._check(b"i-10e", -10)
+
+    def test_long(self):
+        self._check(b"i12345678901234567890e", 12345678901234567890)
+        self._check(b"i-12345678901234567890e", -12345678901234567890)
+
+    def test_string(self):
+        self._check(b"0:", b"")
+        self._check(b"3:abc", b"abc")
+        self._check(b"10:1234567890", b"1234567890")
+
+    def test_list(self):
+        self._check(b"le", [])
+        self._check(b"li1ei2ei3ee", [1, 2, 3])
+        self._check(b"ll5:Alice3:Bobeli2ei3eee", [[b"Alice", b"Bob"], [2, 3]])
+
+    def test_list_as_tuple(self):
+        self._check(b"le", ())
+        self._check(b"li1ei2ei3ee", (1, 2, 3))
+        self._check(b"ll5:Alice3:Bobeli2ei3eee", ((b"Alice", b"Bob"), (2, 3)))
+
+    def test_list_deep_nested(self):
+        if self.id().endswith("(C)"):
+            self.skipTest("no limit recursion in Rust code")
+
+        top = []
+        lst = top
+        for unused_i in range(1000):
+            lst.append([])
+            lst = lst[0]
+        with RecursionLimit():
+            self.assertRaises(RuntimeError, self.module.bencode, top)
+
+    def test_dict(self):
+        self._check(b"de", {})
+        self._check(b"d3:agei25e4:eyes4:bluee", {b"age": 25, b"eyes": b"blue"})
+        self._check(
+            b"d8:spam.mp3d6:author5:Alice6:lengthi100000eee",
+            {b"spam.mp3": {b"author": b"Alice", b"length": 100000}},
+        )
+
+    def test_dict_deep_nested(self):
+        if self.id().endswith("(C)"):
+            self.skipTest("no limit of recursion in Rust code")
+
+        d = top = {}
+        for i in range(1000):
+            d[b""] = {}
+            d = d[b""]
+        with RecursionLimit():
+            self.assertRaises(RuntimeError, self.module.bencode, top)
+
+    def test_bencached(self):
+        self._check(b"i3e", self.module.Bencached(self.module.bencode(3)))
+
+    def test_invalid_dict(self):
+        self.assertRaises(TypeError, self.module.bencode, {1: b"foo"})
+
+    def test_bool(self):
+        self._check(b"i1e", True)
+        self._check(b"i0e", False)
+
+
+class TestBencodeEncodeUtf8(TestCase):
+    module = None
+
+    def _check(self, expected, source):
+        self.assertEqual(expected, self.module.bencode_utf8(source))
+
+    def test_string(self):
+        self._check(b"0:", "")
+        self._check(b"3:abc", "abc")
+        self._check(b"10:1234567890", "1234567890")
+
+    def test_list(self):
+        self._check(b"le", [])
+        self._check(b"li1ei2ei3ee", [1, 2, 3])
+        self._check(b"ll5:Alice3:Bobeli2ei3eee", [["Alice", "Bob"], [2, 3]])
+
+    def test_list_as_tuple(self):
+        self._check(b"le", ())
+        self._check(b"li1ei2ei3ee", (1, 2, 3))
+        self._check(b"ll5:Alice3:Bobeli2ei3eee", (("Alice", "Bob"), (2, 3)))
+
+    def test_dict(self):
+        self._check(b"de", {})
+        self._check(b"d3:agei25e4:eyes4:bluee", {b"age": 25, b"eyes": "blue"})
+        self._check(
+            b"d8:spam.mp3d6:author5:Alice6:lengthi100000eee",
+            {b"spam.mp3": {b"author": b"Alice", b"length": 100000}},
+        )
