Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-mutf8 for openSUSE:Factory 
checked in at 2026-07-14 13:49:27
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-mutf8 (Old)
 and      /work/SRC/openSUSE:Factory/.python-mutf8.new.1991 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-mutf8"

Tue Jul 14 13:49:27 2026 rev:3 rq:1365421 version:1.1.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-mutf8/python-mutf8.changes        
2025-06-05 20:36:39.675086319 +0200
+++ /work/SRC/openSUSE:Factory/.python-mutf8.new.1991/python-mutf8.changes      
2026-07-14 13:50:39.728491670 +0200
@@ -1,0 +2,14 @@
+Mon Jul 13 16:33:44 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 1.1.0:
+  * Supports python 3.9 to 3.14.
+  * Fixed 3 issues with surrogate pair decoding & encoding
+    (closes #6, #4, #5).
+  * *Significantly* improved performance on recent Python
+    versions when decoding MUTF-8 strings that are really UTF-8
+    (the typical case 90% of the time), improving decodes of 1kb
+    strings by ~5x and 64kb strings by ~9x. Encoding of 1kb
+    strings is ~12x faster and 64kb strings are ~48x faster.
+  * Reduced encoding allocations for less memory churn.
+
+-------------------------------------------------------------------

Old:
----
  mutf8-1.0.6.tar.gz

New:
----
  mutf8-1.1.0.tar.gz

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

Other differences:
------------------
++++++ python-mutf8.spec ++++++
--- /var/tmp/diff_new_pack.HNb0g5/_old  2026-07-14 13:50:40.204508056 +0200
+++ /var/tmp/diff_new_pack.HNb0g5/_new  2026-07-14 13:50:40.204508056 +0200
@@ -1,7 +1,7 @@
 #
 # spec file for package python-mutf8
 #
-# Copyright (c) 2025 SUSE LLC
+# Copyright (c) 2026 SUSE LLC and contributors
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -17,7 +17,7 @@
 
 
 Name:           python-mutf8
-Version:        1.0.6
+Version:        1.1.0
 Release:        0
 Summary:        Python/C encoders/decoders for MUTF-8/CESU-8
 License:        MIT

++++++ mutf8-1.0.6.tar.gz -> mutf8-1.1.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mutf8-1.0.6/.bumpversion.cfg 
new/mutf8-1.1.0/.bumpversion.cfg
--- old/mutf8-1.0.6/.bumpversion.cfg    2021-12-29 04:00:07.000000000 +0100
+++ new/mutf8-1.1.0/.bumpversion.cfg    1970-01-01 01:00:00.000000000 +0100
@@ -1,6 +0,0 @@
-[bumpversion]
-current_version = 1.0.6
-commit = True
-tag = True
-
-[bumpversion:file:setup.py]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mutf8-1.0.6/.github/workflows/release.yml 
new/mutf8-1.1.0/.github/workflows/release.yml
--- old/mutf8-1.0.6/.github/workflows/release.yml       2021-12-29 
04:00:07.000000000 +0100
+++ new/mutf8-1.1.0/.github/workflows/release.yml       2026-07-10 
20:04:18.000000000 +0200
@@ -1,99 +1,71 @@
+name: CI
+
 on:
   push:
-    # Sequence of patterns matched against refs/tags
-    tags:
-      - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
-
-name: Release
+    branches: [master]
+  pull_request:
+  release:
+    types: [published]
+  workflow_dispatch:
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: ${{ github.event_name == 'pull_request' }}
 
 jobs:
-  build:
-    name: Creating Windows and OSX wheels
-
+  build_wheels:
+    name: Wheels on ${{ matrix.os }}
     runs-on: ${{ matrix.os }}
     strategy:
       fail-fast: false
       matrix:
-        os: [macos-latest, windows-latest]
-        python-version: [3.6, 3.7, 3.8, 3.9]
+        os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-14]
 
     steps:
-    - uses: actions/checkout@v2
+      - uses: actions/checkout@v7
 
-    - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v2
-      with:
-        python-version: ${{ matrix.python-version }}
-
-    - name: Install python dependencies
-      run: |
-        python -m pip install --upgrade pip
-        python -m pip install --upgrade setuptools
-        pip install wheel twine
-        pip install -e '.'
-
-    - name: Publishing
-      env:
-        TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
-        TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
-      run: |
-        python setup.py bdist_wheel
-        twine upload dist/*
+      - name: Build and test wheels
+        uses: pypa/[email protected]
 
-  build_manylinux2010:
-    name: Creating manylinux releases
+      - uses: actions/upload-artifact@v7
+        with:
+          name: wheels-${{ matrix.os }}
+          path: ./wheelhouse/*.whl
 
+  build_sdist:
+    name: Source distribution
     runs-on: ubuntu-latest
-
     steps:
-    - uses: actions/checkout@v2
-
-    - name: Build (manylinux2014)
-      uses: RalfG/[email protected]_x86_64
-      with:
-        python-versions: 'cp36-cp36m cp37-cp37m cp38-cp38 cp39-cp39'
-
-    - name: Set up Python
-      uses: actions/setup-python@v1
-      with:
-        python-version: 3.8
-
-    - name: Install python dependencies
-      run: |
-        python -m pip install --upgrade pip
-        python -m pip install --upgrade setuptools
-        pip install wheel twine
-
-    - name: Publishing
-      env:
-        TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
-        TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
-      run: |
-        twine upload dist/*-manylinux*.whl
-
-  sdist:
-    name: Creating source release
+      - uses: actions/checkout@v7
 
+      - name: Install uv
+        uses: astral-sh/[email protected]
+        with:
+          enable-cache: true
+
+      - name: Build sdist
+        run: uv build --sdist
+
+      - uses: actions/upload-artifact@v7
+        with:
+          name: sdist
+          path: ./dist/*.tar.gz
+
+  publish:
+    name: Publish to PyPI
+    needs: [build_wheels, build_sdist]
     runs-on: ubuntu-latest
-
+    if: github.event_name == 'release' && github.repository == 'TkTech/mutf8'
+    environment:
+      name: pypi
+      url: https://pypi.org/p/mutf8
+    permissions:
+      id-token: write
     steps:
-    - uses: actions/checkout@v2
+      - uses: actions/download-artifact@v8
+        with:
+          path: dist
+          merge-multiple: true
 
-    - name: Set up Python
-      uses: actions/setup-python@v1
-      with:
-        python-version: 3.8
-
-    - name: Install python dependencies
-      run: |
-        python -m pip install --upgrade pip
-        python -m pip install --upgrade setuptools
-        pip install wheel twine
-
-    - name: Publishing
-      env:
-        TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
-        TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
-      run: |
-        python setup.py sdist
-        twine upload dist/*
+      - name: Publish
+        uses: pypa/gh-action-pypi-publish@release/v1
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mutf8-1.0.6/.github/workflows/test.yml 
new/mutf8-1.1.0/.github/workflows/test.yml
--- old/mutf8-1.0.6/.github/workflows/test.yml  2021-12-29 04:00:07.000000000 
+0100
+++ new/mutf8-1.1.0/.github/workflows/test.yml  1970-01-01 01:00:00.000000000 
+0100
@@ -1,39 +0,0 @@
-name: Tests
-
-on: [push]
-
-jobs:
-  build:
-    name: Running tests.
-
-    runs-on: ubuntu-latest
-    strategy:
-      fail-fast: false
-      matrix:
-        python-version: [3.6, 3.7, 3.8, 3.9]
-
-    steps:
-      # Python needs to be setup before checkout to prevent files from being
-      # left in the source tree. See setup-python/issues/106.
-    - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v2
-      with:
-        python-version: ${{ matrix.python-version }}
-
-    - uses: actions/checkout@v2
-
-    - name: Installing platform dependencies
-      if: matrix.os == 'ubuntu-latest'
-      run: >
-        sudo apt-get install -y
-        build-essential
-        clang-6.0
-
-    - name: Installing python dependencies
-      run: |
-        python -m pip install --upgrade pip
-        pip install -e '.[test]'
-
-    - name: Running tests
-      run: |
-        pytest
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mutf8-1.0.6/.gitignore new/mutf8-1.1.0/.gitignore
--- old/mutf8-1.0.6/.gitignore  2021-12-29 04:00:07.000000000 +0100
+++ new/mutf8-1.1.0/.gitignore  2026-07-10 20:04:18.000000000 +0200
@@ -1,9 +1,14 @@
 *.pyc
 *.pyd
 venv
+.venv/
 .eggs
 cython_debug/*
 build/
 _build/
+dist/
+wheelhouse/
 *.egg-info
 *.so
+uv.lock
+*.idea
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mutf8-1.0.6/README.md new/mutf8-1.1.0/README.md
--- old/mutf8-1.0.6/README.md   2021-12-29 04:00:07.000000000 +0100
+++ new/mutf8-1.1.0/README.md   2026-07-10 20:04:18.000000000 +0200
@@ -1,10 +1,9 @@
-![Tests](https://github.com/TkTech/mutf8/workflows/Tests/badge.svg?branch=master)
+![CI](https://github.com/TkTech/mutf8/actions/workflows/release.yml/badge.svg)
 
 # mutf-8
 
 This package contains simple pure-python as well as C encoders and decoders for
-the MUTF-8 character encoding. In most cases, you can also parse the even-rarer
-CESU-8.
+the MUTF-8 character encoding.
 
 These days, you'll most likely encounter MUTF-8 when working on files or
 protocols related to the JVM. Strings in a Java `.class` file are encoded using
@@ -14,26 +13,15 @@
 This library was extracted from [Lawu][], a Python library for working with JVM
 class files.
 
-## 🎉 Installation
+## Installation
 
+Prebuilt binary packages are available for most platforms and Python versions.
 Install the package from PyPi:
 
 ```
 pip install mutf8
 ```
 
-Binary wheels are available for the following:
-
-|                  | py3.6 | py3.7 | py3.8 | py3.9 |
-| ---------------- | ----- | ----- | ----- | ----- |
-| OS X (x86_64)    | y     | y     | y     | y     |
-| Windows (x86_64) | y     | y     | y     | y     |
-| Linux (x86_64)   | y     | y     | y     | y     |
-
-If binary wheels are not available, it will attempt to build the C extension
-from source with any C99 compiler. If it could not build, it will fall back
-to a pure-python version.
-
 ## Usage
 
 Encoding and decoding is simple:
@@ -48,23 +36,48 @@
 This module *does not* register itself globally as a codec, since importing
 should be side-effect-free.
 
-## 📈 Benchmarks
+## Benchmarks
 
-The C extension is significantly faster - often 20x to 40x faster.
+The C extension is dramatically faster than the pure-Python fallback — from
+~10× on tiny strings (where per-call overhead dominates) to several hundred
+times on realistic payloads, and over 1000× when encoding large ASCII.
 
 <!-- BENCHMARK START -->
 
 ### MUTF-8 Decoding
-| Name                         |   Min (μs) |   Max (μs) |   StdDev |          
 Ops |
-|------------------------------|------------|------------|----------|---------------|
-| cmutf8-decode_modified_utf8  |    0.00009 |    0.00080 |  0.00000 | 
9957678.56358 |
-| pymutf8-decode_modified_utf8 |    0.00190 |    0.06040 |  0.00000 |  
450455.96019 |
+
+| Input | C | Python | Speedup |
+|-------|---|--------|--------:|
+| ASCII (16 B) | 81.5 ± 26 ns | 1.04 ± 0.18 µs | 13× |
+| ASCII (1 KB) | 185 ± 38 ns | 65.7 ± 8.5 µs | 356× |
+| ASCII (64 KB) | 5.58 ± 0.66 µs | 4.35 ± 0.23 ms | 780× |
+| CJK (16 B) | 127 ± 15 ns | 1.33 ± 0.15 µs | 10× |
+| CJK (1 KB) | 681 ± 117 ns | 99.3 ± 9.4 µs | 146× |
+| CJK (64 KB) | 31.8 ± 3.3 µs | 6.28 ± 0.063 ms | 198× |
+| Emoji (16 B) | 100 ± 21 ns | 882 ± 122 ns | 9× |
+| Emoji (1 KB) | 538 ± 119 ns | 79.6 ± 6.9 µs | 148× |
+| Emoji (64 KB) | 26.2 ± 4.1 µs | 5.32 ± 0.13 ms | 203× |
+| Mixed (16 B) | 118 ± 27 ns | 2.17 ± 0.16 µs | 18× |
+| Mixed (1 KB) | 860 ± 162 ns | 76.7 ± 9.2 µs | 89× |
+| Mixed (64 KB) | 38 ± 5 µs | 5.3 ± 0.48 ms | 139× |
 
 ### MUTF-8 Encoding
-| Name                         |   Min (μs) |   Max (μs) |   StdDev |          
  Ops |
-|------------------------------|------------|------------|----------|----------------|
-| cmutf8-encode_modified_utf8  |    0.00008 |    0.00151 |  0.00000 | 
11897361.05101 |
-| pymutf8-encode_modified_utf8 |    0.00180 |    0.16650 |  0.00000 |   
474390.98091 |
+
+| Input | C | Python | Speedup |
+|-------|---|--------|--------:|
+| ASCII (16 B) | 70.5 ± 5.8 ns | 1.23 ± 0.15 µs | 18× |
+| ASCII (1 KB) | 109 ± 16 ns | 49 ± 6.2 µs | 449× |
+| ASCII (64 KB) | 1.55 ± 0.27 µs | 2.93 ± 0.19 ms | 1889× |
+| CJK (16 B) | 81.4 ± 12 ns | 2.02 ± 0.26 µs | 25× |
+| CJK (1 KB) | 624 ± 127 ns | 108 ± 16 µs | 173× |
+| CJK (64 KB) | 32.5 ± 2.9 µs | 6.8 ± 0.24 ms | 209× |
+| Emoji (16 B) | 73 ± 10 ns | 1.17 ± 0.12 µs | 16× |
+| Emoji (1 KB) | 621 ± 76 ns | 67 ± 8.5 µs | 108× |
+| Emoji (64 KB) | 33.3 ± 6 µs | 3.98 ± 0.12 ms | 119× |
+| Mixed (16 B) | 108 ± 97 ns | 2.48 ± 0.35 µs | 23× |
+| Mixed (1 KB) | 1.39 ± 0.29 µs | 63.3 ± 3.5 µs | 45× |
+| Mixed (64 KB) | 81.2 ± 14 µs | 3.91 ± 0.093 ms | 48× |
+
 <!-- BENCHMARK END -->
 
 ## C Extension
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mutf8-1.0.6/mutf8/__init__.py 
new/mutf8-1.1.0/mutf8/__init__.py
--- old/mutf8-1.0.6/mutf8/__init__.py   2021-12-29 04:00:07.000000000 +0100
+++ new/mutf8-1.1.0/mutf8/__init__.py   2026-07-10 20:04:18.000000000 +0200
@@ -10,6 +10,8 @@
     for MUTF-8/CESU-8 into the python core.
 """
 
+__version__ = "1.1.0"
+
 
 try:
     from mutf8.cmutf8 import decode_modified_utf8, encode_modified_utf8
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mutf8-1.0.6/mutf8/cmutf8.c 
new/mutf8-1.1.0/mutf8/cmutf8.c
--- old/mutf8-1.0.6/mutf8/cmutf8.c      2021-12-29 04:00:07.000000000 +0100
+++ new/mutf8-1.1.0/mutf8/cmutf8.c      2026-07-10 20:04:18.000000000 +0200
@@ -1,6 +1,7 @@
 #define PY_SSIZE_T_CLEAN
 #include <Python.h>
 #include <stdint.h>
+#include <string.h>
 
 PyDoc_STRVAR(decode_doc,
              "Decodes a bytestring containing MUTF-8 as defined in section\n"
@@ -32,6 +33,22 @@
 
     // MUTF-8 input.
     uint8_t *buf = (uint8_t *)view.buf;
+
+    // Fast path: without 0xED (leads every surrogate/6-byte form), 0xC0 (the
+    // NUL escape) or a raw 0x00 (illegal), the input is plain UTF-8. Offload
+    // to CPython's optimized decoder; on any error fall back to the strict
+    // loop below so the exception is still reported as mutf-8.
+    if (memchr(buf, 0xED, view.len) == NULL &&
+        memchr(buf, 0xC0, view.len) == NULL &&
+        memchr(buf, 0x00, view.len) == NULL) {
+        PyObject *fast = PyUnicode_DecodeUTF8((const char *)buf, view.len, 
NULL);
+        if (fast != NULL) {
+            PyBuffer_Release(&view);
+            return fast;
+        }
+        PyErr_Clear();
+    }
+
     // Array of temporary UCS-4 codepoints.
     // There's no point using PyUnicode_new and _WriteChar, because
     // it requires us to have iterated the string to get the maximum unicode
@@ -74,33 +91,30 @@
             uint8_t b2 = buf[ix + 1];
             uint8_t b3 = buf[ix + 2];
 
-            if (x == 0xED && (b2 & 0xF0) == 0xA0) {
-                if (ix + 5 >= view.len) {
-                    return_err(
-                        "6-byte codepoint started, but input too short"
-                        " to finish.");
-                }
-
-                // Possible six-byte codepoint.
-                uint8_t b4 = buf[ix + 3];
+            if (x == 0xED && (b2 & 0xF0) == 0xA0 && ix + 5 < view.len &&
+                buf[ix + 3] == 0xED && (buf[ix + 4] & 0xF0) == 0xB0) {
+                // Six-byte codepoint: a supplementary character written as a
+                // UTF-16 surrogate pair, each half encoded as its own 3-byte
+                // sequence. We only take this branch once a low surrogate is
+                // confirmed to follow; otherwise `x b2 b3` is a lone
+                // (unpaired) surrogate and is decoded as a normal 3-byte
+                // codepoint below.
                 uint8_t b5 = buf[ix + 4];
                 uint8_t b6 = buf[ix + 5];
-
-                if (b4 == 0xED && (b5 & 0xF0) == 0xB0) {
-                    // Definite six-byte codepoint.
-                    x = (
-                        0x10000 |
-                        (b2 & 0x0F) << 0x10 |
-                        (b3 & 0x3F) << 0x0A |
-                        (b5 & 0x0F) << 0x06 |
-                        (b6 & 0x3F)
-                    );
-                    ix += 5;
-                    cp_out[cp_count++] = x;
-                    continue;
-                }
+                x = (
+                    0x10000 +
+                    ((b2 & 0x0F) << 0x10 |
+                     (b3 & 0x3F) << 0x0A |
+                     (b5 & 0x0F) << 0x06 |
+                     (b6 & 0x3F))
+                );
+                ix += 5;
+                cp_out[cp_count++] = x;
+                continue;
             }
 
+            // Regular three-byte codepoint. This also covers lone/unpaired
+            // surrogates, each of which is encoded as a single 3-byte 
sequence.
             x = (
                 (x & 0x0F) << 0x0C |
                 (b2 & 0x3F) << 0x06 |
@@ -183,11 +197,23 @@
     void *data = PyUnicode_DATA(src);
     Py_ssize_t length = PyUnicode_GET_LENGTH(src);
     int kind = PyUnicode_KIND(src);
-    char *byte_out = PyMem_Calloc(_encoded_size(data, length, kind), 1);
 
-    if (!byte_out) {
-        return PyErr_NoMemory();
+    // Fast path: with no supplementary char (kind < 4-byte) and no embedded
+    // NUL, MUTF-8 == UTF-8 with "surrogatepass" (same 3-byte form for lone
+    // surrogates). Offload to CPython's optimized encoder.
+    if (kind != PyUnicode_4BYTE_KIND &&
+        PyUnicode_FindChar(src, 0, 0, length, 1) == -1) {
+        return PyUnicode_AsEncodedString(src, "utf-8", "surrogatepass");
+    }
+
+    // Allocate the result up front at its exact size and write straight into
+    // it, avoiding a temporary buffer, its zeroing, and a full-output copy.
+    Py_ssize_t size = _encoded_size(data, length, kind);
+    PyObject *out = PyBytes_FromStringAndSize(NULL, size);
+    if (out == NULL) {
+        return NULL;
     }
+    char *byte_out = PyBytes_AS_STRING(out);
 
     Py_ssize_t byte_count = 0;
 
@@ -214,7 +240,11 @@
             byte_out[byte_count++] = (0x80 | (0x3F & cp));
         }
         else {
-            // "Two-times-three" byte codepoint.
+            // Six-byte codepoint: a supplementary character written as a
+            // UTF-16 surrogate pair, each half encoded as its own 3-byte
+            // sequence. The codepoint is first offset by 0x10000 as required
+            // by the surrogate-pair algorithm.
+            cp -= 0x10000;
             byte_out[byte_count++] = 0xED;
             byte_out[byte_count++] = 0xA0 | ((cp >> 0x10) & 0x0F);
             byte_out[byte_count++] = 0x80 | ((cp >> 0x0A) & 0x3F);
@@ -224,8 +254,6 @@
         }
     }
 
-    PyObject *out = PyBytes_FromStringAndSize(byte_out, byte_count);
-    PyMem_Free(byte_out);
     return out;
 }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mutf8-1.0.6/mutf8/mutf8.py 
new/mutf8-1.1.0/mutf8/mutf8.py
--- old/mutf8-1.0.6/mutf8/mutf8.py      2021-12-29 04:00:07.000000000 +0100
+++ new/mutf8-1.1.0/mutf8/mutf8.py      2026-07-10 20:04:18.000000000 +0200
@@ -59,36 +59,30 @@
             b2 = s[s_ix]
             b3 = s[s_ix + 1]
 
-            if b1 == 0xED and (b2 & 0xF0) == 0xA0:
-                # Possible six-byte codepoint.
-                if s_ix + 4 >= s_len:
-                    raise UnicodeDecodeError(
-                            'mutf-8',
-                            s,
-                            s_ix - 1,
-                            s_ix,
-                            '3-byte or 6-byte codepoint started, but input too'
-                            ' short to finish.'
-                        )
-
-                b4 = s[s_ix + 2]
+            if (b1 == 0xED and (b2 & 0xF0) == 0xA0 and s_ix + 4 < s_len
+                    and s[s_ix + 2] == 0xED and (s[s_ix + 3] & 0xF0) == 0xB0):
+                # Six-byte codepoint: a supplementary character written as a
+                # UTF-16 surrogate pair, each half encoded as its own 3-byte
+                # sequence. Only take this branch once we've confirmed a low
+                # surrogate actually follows; otherwise `b1 b2 b3` is a lone
+                # (unpaired) surrogate and is decoded as a normal 3-byte
+                # codepoint below.
                 b5 = s[s_ix + 3]
                 b6 = s[s_ix + 4]
-
-                if b4 == 0xED and (b5 & 0xF0) == 0xB0:
-                    # Definite six-byte codepoint.
-                    s_out.append(
-                        chr(
-                            0x10000 |
-                            (b2 & 0x0F) << 0x10 |
-                            (b3 & 0x3F) << 0x0A |
-                            (b5 & 0x0F) << 0x06 |
-                            (b6 & 0x3F)
-                        )
+                s_out.append(
+                    chr(
+                        0x10000 +
+                        ((b2 & 0x0F) << 0x10 |
+                         (b3 & 0x3F) << 0x0A |
+                         (b5 & 0x0F) << 0x06 |
+                         (b6 & 0x3F))
                     )
-                    s_ix += 5
-                    continue
+                )
+                s_ix += 5
+                continue
 
+            # Regular three-byte codepoint. This also covers lone/unpaired
+            # surrogates, each of which is encoded as a single 3-byte sequence.
             s_out.append(
                 chr(
                     (b1 & 0x0F) << 0x0C |
@@ -134,14 +128,18 @@
                 (0x80 | (0x3F & c))
             ])
         else:
-            # Six-byte codepoint.
+            # Six-byte codepoint: a supplementary character written as a
+            # UTF-16 surrogate pair, each half encoded as its own 3-byte
+            # sequence. The codepoint is first offset by 0x10000 as required
+            # by the surrogate-pair algorithm.
+            c -= 0x10000
             final_string.extend([
                 0xED,
                 0xA0 | ((c >> 0x10) & 0x0F),
-                0x80 | ((c >> 0x0A) & 0x3f),
+                0x80 | ((c >> 0x0A) & 0x3F),
                 0xED,
-                0xb0 | ((c >> 0x06) & 0x0f),
-                0x80 | (c & 0x3f)
+                0xB0 | ((c >> 0x06) & 0x0F),
+                0x80 | (c & 0x3F)
             ])
 
     return bytes(final_string)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mutf8-1.0.6/pyproject.toml 
new/mutf8-1.1.0/pyproject.toml
--- old/mutf8-1.0.6/pyproject.toml      1970-01-01 01:00:00.000000000 +0100
+++ new/mutf8-1.1.0/pyproject.toml      2026-07-10 20:04:18.000000000 +0200
@@ -0,0 +1,66 @@
+[build-system]
+requires = ["setuptools>=77"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "mutf8"
+dynamic = ["version"]
+description = "Fast MUTF-8 encoder & decoder"
+readme = "README.md"
+requires-python = ">=3.9"
+license = "MIT"
+license-files = ["LICENCE"]
+authors = [{ name = "Tyler Kennedy", email = "[email protected]" }]
+keywords = ["mutf-8", "cesu-8", "jvm"]
+classifiers = [
+    "Development Status :: 5 - Production/Stable",
+    "Intended Audience :: Developers",
+    "Operating System :: OS Independent",
+    "Programming Language :: C",
+    "Programming Language :: Python :: 3",
+    "Programming Language :: Python :: 3.9",
+    "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",
+    "Topic :: Software Development :: Libraries :: Python Modules",
+]
+
+[project.optional-dependencies]
+test = ["pytest", "pytest-benchmark"]
+
+[project.urls]
+Homepage = "https://github.com/TkTech/mutf8";
+Source = "https://github.com/TkTech/mutf8";
+Issues = "https://github.com/TkTech/mutf8/issues";
+
+[tool.setuptools]
+packages = ["mutf8"]
+# optional=true so a failed compile falls back to the pure-Python 
mutf8/mutf8.py.
+ext-modules = [
+    { name = "mutf8.cmutf8", sources = ["mutf8/cmutf8.c"], language = "c", 
optional = true },
+]
+
+[tool.setuptools.dynamic]
+version = { attr = "mutf8.__version__" }
+
+[tool.pytest.ini_options]
+testpaths = ["tests"]
+markers = ["slow: mark test as slow to run"]
+
+[tool.cibuildwheel]
+skip = ["*_i686"]
+test-extras = ["test"]
+test-command = "pytest {project}/tests"
+
+[tool.bumpversion]
+current_version = "1.1.0"
+commit = true
+tag = true
+tag_name = "v{new_version}"
+allow_dirty = false
+
+[[tool.bumpversion.files]]
+filename = "mutf8/__init__.py"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mutf8-1.0.6/setup.py new/mutf8-1.1.0/setup.py
--- old/mutf8-1.0.6/setup.py    2021-12-29 04:00:07.000000000 +0100
+++ new/mutf8-1.1.0/setup.py    1970-01-01 01:00:00.000000000 +0100
@@ -1,43 +0,0 @@
-import os
-import os.path
-
-from setuptools import setup, find_packages, Extension
-
-
-root = os.path.abspath(os.path.dirname(__file__))
-with open(os.path.join(root, 'README.md'), 'rb') as readme:
-    long_description = readme.read().decode('utf-8')
-
-
-setup(
-    name='mutf8',
-    packages=find_packages(),
-    version='1.0.6',
-    description='Fast MUTF-8 encoder & decoder',
-    long_description=long_description,
-    long_description_content_type='text/markdown',
-    author='Tyler Kennedy',
-    author_email='[email protected]',
-    url='http://github.com/TkTech/mutf8',
-    keywords=['mutf-8', 'cesu-8', 'jvm'],
-    classifiers=[
-        'Programming Language :: Python :: 3',
-        'License :: OSI Approved :: MIT License',
-        'Operating System :: OS Independent',
-        'Intended Audience :: Developers',
-    ],
-    extras_require={
-        'test': [
-            'pytest',
-            'pytest-benchmark'
-        ]
-    },
-    ext_modules=[
-        Extension(
-            'mutf8.cmutf8',
-            ['mutf8/cmutf8.c'],
-            language='c',
-            optional=True
-        )
-    ]
-)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mutf8-1.0.6/tests/test_benchmark.py 
new/mutf8-1.1.0/tests/test_benchmark.py
--- old/mutf8-1.0.6/tests/test_benchmark.py     2021-12-29 04:00:07.000000000 
+0100
+++ new/mutf8-1.1.0/tests/test_benchmark.py     2026-07-10 20:04:18.000000000 
+0200
@@ -1,28 +1,67 @@
+"""
+Benchmarks comparing the pure-Python and C encoders/decoders across realistic
+inputs (ASCII, CJK, emoji, mixed) and payload sizes.
+
+Marked ``slow``; run with ``pytest --runslow tests/test_benchmark.py``.
+"""
 import pytest
 
 import mutf8.mutf8 as pymutf8
 import mutf8.cmutf8 as cmutf8
 
+# Short representative units, repeated to hit each target size. Ordered from
+# the most common real-world case (ASCII JVM symbols) to the rarest.
+_UNITS = {
+    "ascii": "java/lang/Object;",           # 1-byte codepoints
+    "cjk": "編碼測試字",                       # 3-byte codepoints
+    "emoji": "🚀🎉",                          # 6-byte (surrogate pair) 
codepoints
+    "mixed": "café ☕ item#42 shipped 🚚 ",   # 1/2/3/6-byte mix
+}
+
+# Target *encoded* sizes: 16b exposes per-call overhead, the rest throughput.
+_SIZES = {"16b": 16, "1kb": 1024, "64kb": 64 * 1024}
+
+
+def _scale(unit, target_bytes):
+    reps = max(1, round(target_bytes / len(cmutf8.encode_modified_utf8(unit))))
+    return unit * reps
+
+
+# (content, size) -> decoded str and its MUTF-8 encoding.
+STR_CORPORA = {
+    (content, size): _scale(unit, nbytes)
+    for content, unit in _UNITS.items()
+    for size, nbytes in _SIZES.items()
+}
+BYTES_CORPORA = {
+    key: cmutf8.encode_modified_utf8(text) for key, text in STR_CORPORA.items()
+}
+
+CORPORA = list(STR_CORPORA)
+_CORPUS_IDS = [f"{content}-{size}" for content, size in CORPORA]
+_IMPLS = (("py", pymutf8), ("c", cmutf8))
+_IMPL_IDS = [name for name, _ in _IMPLS]
+
 
[email protected]('group,decoder', (
-    ('pymutf8', pymutf8.decode_modified_utf8),
-    ('cmutf8', cmutf8.decode_modified_utf8)
-))
 @pytest.mark.slow
-def test_decode(group, decoder, benchmark):
-    """Compare the performance of the python and C decoders."""
-    benchmark.group = 'MUTF-8 Decoding'
-    benchmark.extra_info['group'] = group
-    benchmark(decoder, b'\xED\xA0\xBD\xED\xB8\x88')
[email protected]("corpus", CORPORA, ids=_CORPUS_IDS)
[email protected]("impl,module", _IMPLS, ids=_IMPL_IDS)
+def test_decode(corpus, impl, module, benchmark):
+    content, size = corpus
+    data = BYTES_CORPORA[corpus]
+    benchmark.group = f"decode {content}-{size}"
+    benchmark.extra_info.update(impl=impl, encoded_bytes=len(data))
+    result = benchmark(module.decode_modified_utf8, data)
+    assert result == STR_CORPORA[corpus]
 
 
[email protected]('group,encoder', (
-    ('pymutf8', pymutf8.encode_modified_utf8),
-    ('cmutf8', cmutf8.encode_modified_utf8)
-))
 @pytest.mark.slow
-def test_encode(group, encoder, benchmark):
-    """Compare the performance of the python and C encoders."""
-    benchmark.group = 'MUTF-8 Encoding'
-    benchmark.extra_info['group'] = group
-    benchmark(encoder, '\U0001F608')
[email protected]("corpus", CORPORA, ids=_CORPUS_IDS)
[email protected]("impl,module", _IMPLS, ids=_IMPL_IDS)
+def test_encode(corpus, impl, module, benchmark):
+    content, size = corpus
+    text = STR_CORPORA[corpus]
+    benchmark.group = f"encode {content}-{size}"
+    benchmark.extra_info.update(impl=impl, 
encoded_bytes=len(BYTES_CORPORA[corpus]))
+    result = benchmark(module.encode_modified_utf8, text)
+    assert result == BYTES_CORPORA[corpus]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mutf8-1.0.6/tests/test_bugs.py 
new/mutf8-1.1.0/tests/test_bugs.py
--- old/mutf8-1.0.6/tests/test_bugs.py  2021-12-29 04:00:07.000000000 +0100
+++ new/mutf8-1.1.0/tests/test_bugs.py  2026-07-10 20:04:18.000000000 +0200
@@ -32,3 +32,61 @@
     """
     str = '黑人抬棺組裝包'
     assert decoder(encoder(str)) == str
+
+
+# Supplementary characters and their *correct* MUTF-8 encoding, cross-checked
+# against Java's DataOutputStream.writeUTF, Android/DEX and Rust's
+# residua-mutf8. See https://github.com/TkTech/mutf8/issues/5 and /issues/6.
+ISSUE_5_6_VECTORS = (
+    ('\U00010000', b'\xed\xa0\x80\xed\xb0\x80'),
+    ('\U00020000', b'\xed\xa1\x80\xed\xb0\x80'),
+    ('\U00040000', b'\xed\xa3\x80\xed\xb0\x80'),
+    ('\U00080000', b'\xed\xa7\x80\xed\xb0\x80'),
+    ('\U00100000', b'\xed\xaf\x80\xed\xb0\x80'),
+    ('\U0010FFFF', b'\xed\xaf\xbf\xed\xbf\xbf'),
+    ('\U00010401', b'\xed\xa0\x81\xed\xb0\x81'),   # residua-mutf8 example
+    ('\U0001F4CB', b'\xed\xa0\xbd\xed\xb3\x8b'),   # 📋
+    ('\U0001F3F9', b'\xed\xa0\xbc\xed\xbf\xb9'),   # 🏹
+    ('\U0001F41F', b'\xed\xa0\xbd\xed\xb0\x9f'),   # 🐟
+)
+
+
+def test_issue_5_and_6(encoder, decoder):
+    """
+    Supplementary characters (> U+FFFF) must be encoded as a UTF-16 surrogate
+    pair after subtracting 0x10000, matching Java/Android/CESU-8. Previously
+    the 0x10000 offset was omitted on encode (and compensated with a bitwise
+    OR on decode), producing non-standard bytes and losing data for plane 16.
+
+    https://github.com/TkTech/mutf8/issues/5
+    https://github.com/TkTech/mutf8/issues/6
+    """
+    for text, expected in ISSUE_5_6_VECTORS:
+        assert encoder(text) == expected, hex(ord(text))
+        # Decoding standard MUTF-8 must recover the original codepoint.
+        assert decoder(expected) == text, hex(ord(text))
+        # And a full round-trip must be lossless across the whole range.
+        assert decoder(encoder(text)) == text, hex(ord(text))
+
+
+def test_issue_4(decoder, encoder):
+    """
+    A lone (unpaired) high surrogate is legal in a Java string and is encoded
+    as a single 3-byte sequence (0xED 0xA0-0xAF ...). The decoder must not
+    assume every 0xED 0xAx sequence begins a six-byte pair; when no low
+    surrogate follows (including at end-of-input) it must decode a 3-byte
+    lone surrogate instead of raising "input too short".
+
+    https://github.com/TkTech/mutf8/issues/4
+    """
+    # A lone high surrogate at end-of-input (previously raised spuriously).
+    assert decoder(b'\xed\xa1\x80') == '\ud840'
+    # A lone low surrogate, likewise.
+    assert decoder(b'\xed\xb0\x80') == '\udc00'
+    # A high surrogate followed by non-surrogate data, not a pair.
+    assert decoder(b'\xed\xa1\x80AB') == '\ud840AB'
+    # Two high surrogates in a row: each is its own 3-byte codepoint.
+    assert decoder(b'\xed\xa1\x80\xed\xa1\x80') == '\ud840\ud840'
+    # Lone surrogates must survive a round-trip.
+    for cp in (0xD800, 0xD840, 0xDBFF, 0xDC00, 0xDFFF):
+        assert decoder(encoder(chr(cp))) == chr(cp), hex(cp)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mutf8-1.0.6/tests/test_modified_utf8.py 
new/mutf8-1.1.0/tests/test_modified_utf8.py
--- old/mutf8-1.0.6/tests/test_modified_utf8.py 2021-12-29 04:00:07.000000000 
+0100
+++ new/mutf8-1.1.0/tests/test_modified_utf8.py 2026-07-10 20:04:18.000000000 
+0200
@@ -125,44 +125,44 @@
     Test data taken from py2jdbc.
     """
     pairs = _pairs(
-        (0x10000, (0xed, 0xa1, 0x80, 0xed, 0xb0, 0x80)),
-        (0x10001, (0xed, 0xa1, 0x80, 0xed, 0xb0, 0x81)),
-        (0x10002, (0xed, 0xa1, 0x80, 0xed, 0xb0, 0x82)),
-        (0x10004, (0xed, 0xa1, 0x80, 0xed, 0xb0, 0x84)),
-        (0x10008, (0xed, 0xa1, 0x80, 0xed, 0xb0, 0x88)),
-        (0x10010, (0xed, 0xa1, 0x80, 0xed, 0xb0, 0x90)),
-        (0x10020, (0xed, 0xa1, 0x80, 0xed, 0xb0, 0xa0)),
-        (0x10040, (0xed, 0xa1, 0x80, 0xed, 0xb1, 0x80)),
-        (0x10080, (0xed, 0xa1, 0x80, 0xed, 0xb2, 0x80)),
-        (0x10100, (0xed, 0xa1, 0x80, 0xed, 0xb4, 0x80)),
-        (0x10200, (0xed, 0xa1, 0x80, 0xed, 0xb8, 0x80)),
-        (0x10400, (0xed, 0xa1, 0x81, 0xed, 0xb0, 0x80)),
-        (0x10800, (0xed, 0xa1, 0x82, 0xed, 0xb0, 0x80)),
-        (0x11000, (0xed, 0xa1, 0x84, 0xed, 0xb0, 0x80)),
-        (0x12000, (0xed, 0xa1, 0x88, 0xed, 0xb0, 0x80)),
-        (0x14000, (0xed, 0xa1, 0x90, 0xed, 0xb0, 0x80)),
-        (0x18000, (0xed, 0xa1, 0xa0, 0xed, 0xb0, 0x80)),
-        (0x30000, (0xed, 0xa3, 0x80, 0xed, 0xb0, 0x80)),
-        (0x50000, (0xed, 0xa5, 0x80, 0xed, 0xb0, 0x80)),
-        (0x90000, (0xed, 0xa9, 0x80, 0xed, 0xb0, 0x80)),
-        (0x10003, (0xed, 0xa1, 0x80, 0xed, 0xb0, 0x83)),
-        (0x10007, (0xed, 0xa1, 0x80, 0xed, 0xb0, 0x87)),
-        (0x1000f, (0xed, 0xa1, 0x80, 0xed, 0xb0, 0x8f)),
-        (0x1001f, (0xed, 0xa1, 0x80, 0xed, 0xb0, 0x9f)),
-        (0x1003f, (0xed, 0xa1, 0x80, 0xed, 0xb0, 0xbf)),
-        (0x1007f, (0xed, 0xa1, 0x80, 0xed, 0xb1, 0xbf)),
-        (0x100ff, (0xed, 0xa1, 0x80, 0xed, 0xb3, 0xbf)),
-        (0x101ff, (0xed, 0xa1, 0x80, 0xed, 0xb7, 0xbf)),
-        (0x103ff, (0xed, 0xa1, 0x80, 0xed, 0xbf, 0xbf)),
-        (0x107ff, (0xed, 0xa1, 0x81, 0xed, 0xbf, 0xbf)),
-        (0x10fff, (0xed, 0xa1, 0x83, 0xed, 0xbf, 0xbf)),
-        (0x11fff, (0xed, 0xa1, 0x87, 0xed, 0xbf, 0xbf)),
-        (0x13fff, (0xed, 0xa1, 0x8f, 0xed, 0xbf, 0xbf)),
-        (0x17fff, (0xed, 0xa1, 0x9f, 0xed, 0xbf, 0xbf)),
-        (0x1ffff, (0xed, 0xa1, 0xbf, 0xed, 0xbf, 0xbf)),
-        (0x3ffff, (0xed, 0xa3, 0xbf, 0xed, 0xbf, 0xbf)),
-        (0x7ffff, (0xed, 0xa7, 0xbf, 0xed, 0xbf, 0xbf)),
-        (0xfffff, (0xed, 0xaf, 0xbf, 0xed, 0xbf, 0xbf))
+        (0x10000, (0xed, 0xa0, 0x80, 0xed, 0xb0, 0x80)),
+        (0x10001, (0xed, 0xa0, 0x80, 0xed, 0xb0, 0x81)),
+        (0x10002, (0xed, 0xa0, 0x80, 0xed, 0xb0, 0x82)),
+        (0x10004, (0xed, 0xa0, 0x80, 0xed, 0xb0, 0x84)),
+        (0x10008, (0xed, 0xa0, 0x80, 0xed, 0xb0, 0x88)),
+        (0x10010, (0xed, 0xa0, 0x80, 0xed, 0xb0, 0x90)),
+        (0x10020, (0xed, 0xa0, 0x80, 0xed, 0xb0, 0xa0)),
+        (0x10040, (0xed, 0xa0, 0x80, 0xed, 0xb1, 0x80)),
+        (0x10080, (0xed, 0xa0, 0x80, 0xed, 0xb2, 0x80)),
+        (0x10100, (0xed, 0xa0, 0x80, 0xed, 0xb4, 0x80)),
+        (0x10200, (0xed, 0xa0, 0x80, 0xed, 0xb8, 0x80)),
+        (0x10400, (0xed, 0xa0, 0x81, 0xed, 0xb0, 0x80)),
+        (0x10800, (0xed, 0xa0, 0x82, 0xed, 0xb0, 0x80)),
+        (0x11000, (0xed, 0xa0, 0x84, 0xed, 0xb0, 0x80)),
+        (0x12000, (0xed, 0xa0, 0x88, 0xed, 0xb0, 0x80)),
+        (0x14000, (0xed, 0xa0, 0x90, 0xed, 0xb0, 0x80)),
+        (0x18000, (0xed, 0xa0, 0xa0, 0xed, 0xb0, 0x80)),
+        (0x30000, (0xed, 0xa2, 0x80, 0xed, 0xb0, 0x80)),
+        (0x50000, (0xed, 0xa4, 0x80, 0xed, 0xb0, 0x80)),
+        (0x90000, (0xed, 0xa8, 0x80, 0xed, 0xb0, 0x80)),
+        (0x10003, (0xed, 0xa0, 0x80, 0xed, 0xb0, 0x83)),
+        (0x10007, (0xed, 0xa0, 0x80, 0xed, 0xb0, 0x87)),
+        (0x1000f, (0xed, 0xa0, 0x80, 0xed, 0xb0, 0x8f)),
+        (0x1001f, (0xed, 0xa0, 0x80, 0xed, 0xb0, 0x9f)),
+        (0x1003f, (0xed, 0xa0, 0x80, 0xed, 0xb0, 0xbf)),
+        (0x1007f, (0xed, 0xa0, 0x80, 0xed, 0xb1, 0xbf)),
+        (0x100ff, (0xed, 0xa0, 0x80, 0xed, 0xb3, 0xbf)),
+        (0x101ff, (0xed, 0xa0, 0x80, 0xed, 0xb7, 0xbf)),
+        (0x103ff, (0xed, 0xa0, 0x80, 0xed, 0xbf, 0xbf)),
+        (0x107ff, (0xed, 0xa0, 0x81, 0xed, 0xbf, 0xbf)),
+        (0x10fff, (0xed, 0xa0, 0x83, 0xed, 0xbf, 0xbf)),
+        (0x11fff, (0xed, 0xa0, 0x87, 0xed, 0xbf, 0xbf)),
+        (0x13fff, (0xed, 0xa0, 0x8f, 0xed, 0xbf, 0xbf)),
+        (0x17fff, (0xed, 0xa0, 0x9f, 0xed, 0xbf, 0xbf)),
+        (0x1ffff, (0xed, 0xa0, 0xbf, 0xed, 0xbf, 0xbf)),
+        (0x3ffff, (0xed, 0xa2, 0xbf, 0xed, 0xbf, 0xbf)),
+        (0x7ffff, (0xed, 0xa6, 0xbf, 0xed, 0xbf, 0xbf)),
+        (0xfffff, (0xed, 0xae, 0xbf, 0xed, 0xbf, 0xbf))
     )
 
     for decoded, original in pairs:

Reply via email to