Control: tags 1070712 + patch
Control: tags 1070712 + pending
Control: tags 1088325 + patch
Control: tags 1088325 + pending

Dear maintainer,

I've prepared an NMU for jinja2 (versioned as 3.1.3-1.1) and uploaded
it to DELAYED/7. Please feel free to tell me if I should cancel it.

cu
Adrian
diffstat for jinja2-3.1.3 jinja2-3.1.3

 changelog                                                               |    8 
 control                                                                 |    1 
 patches/0001-test-on-trio-fix-all-missing-aclose-related-warnings.patch |  600 ++++++++++
 patches/0002-fix-test_package_zip_list-on-3.13.patch                    |   69 +
 patches/0003-disallow-invalid-characters-in-keys-to-xmlattr-filte.patch |   89 +
 patches/series                                                          |    3 
 6 files changed, 770 insertions(+)

diff -Nru jinja2-3.1.3/debian/changelog jinja2-3.1.3/debian/changelog
--- jinja2-3.1.3/debian/changelog	2024-03-05 10:32:06.000000000 +0200
+++ jinja2-3.1.3/debian/changelog	2024-12-08 07:59:01.000000000 +0200
@@ -1,3 +1,11 @@
+jinja2 (3.1.3-1.1) unstable; urgency=medium
+
+  * Non-maintainer upload.
+  * Backport fixes for FTBFS with Python 3.13. (Closes: #1088325)
+  * CVE-2024-34064: HTML attribute injection (Closes: #1070712)
+
+ -- Adrian Bunk <b...@debian.org>  Sun, 08 Dec 2024 07:59:01 +0200
+
 jinja2 (3.1.3-1) unstable; urgency=medium
 
   * Team upload.
diff -Nru jinja2-3.1.3/debian/control jinja2-3.1.3/debian/control
--- jinja2-3.1.3/debian/control	2024-03-05 10:19:30.000000000 +0200
+++ jinja2-3.1.3/debian/control	2024-12-08 07:59:01.000000000 +0200
@@ -11,6 +11,7 @@
  python3-babel,
  python3-markupsafe (>= 2.0),
  python3-pallets-sphinx-themes (>= 2.0.2),
+ python3-trio,
  python3-pygments,
  python3-pytest,
  python3-setuptools,
diff -Nru jinja2-3.1.3/debian/patches/0001-test-on-trio-fix-all-missing-aclose-related-warnings.patch jinja2-3.1.3/debian/patches/0001-test-on-trio-fix-all-missing-aclose-related-warnings.patch
--- jinja2-3.1.3/debian/patches/0001-test-on-trio-fix-all-missing-aclose-related-warnings.patch	1970-01-01 02:00:00.000000000 +0200
+++ jinja2-3.1.3/debian/patches/0001-test-on-trio-fix-all-missing-aclose-related-warnings.patch	2024-12-08 07:58:50.000000000 +0200
@@ -0,0 +1,600 @@
+From fe03877c56f1a9c0f4fbe1b89f1ed010f3f1b210 Mon Sep 17 00:00:00 2001
+From: Thomas Grainger <tagr...@gmail.com>
+Date: Sat, 11 May 2024 23:01:12 +0100
+Subject: test on trio, fix all missing aclose related warnings (#1960)
+
+---
+ requirements/docs.txt       |   2 +-
+ requirements/tests.txt      |  20 +++++-
+ src/jinja2/async_utils.py   |  25 ++++++--
+ src/jinja2/compiler.py      |  44 ++++++++-----
+ src/jinja2/environment.py   |  12 +++-
+ tests/test_async.py         | 122 +++++++++++++++++++++++++++++-------
+ tests/test_async_filters.py |  67 ++++++++++++++++----
+ 7 files changed, 230 insertions(+), 62 deletions(-)
+
+diff --git a/requirements/docs.txt b/requirements/docs.txt
+index e125c59..27488ad 100644
+--- a/requirements/docs.txt
++++ b/requirements/docs.txt
+@@ -15,7 +15,7 @@ charset-normalizer==3.1.0
+     # via requests
+ docutils==0.20.1
+     # via sphinx
+-idna==3.4
++idna==3.6
+     # via requests
+ imagesize==1.4.1
+     # via sphinx
+diff --git a/requirements/tests.txt b/requirements/tests.txt
+index 6168271..bb8f55d 100644
+--- a/requirements/tests.txt
++++ b/requirements/tests.txt
+@@ -1,19 +1,35 @@
+-# SHA1:0eaa389e1fdb3a1917c0f987514bd561be5718ee
++# SHA1:b8d151f902b43c4435188a9d3494fb8d4af07168
+ #
+ # This file is autogenerated by pip-compile-multi
+ # To update, run:
+ #
+ #    pip-compile-multi
+ #
++attrs==23.2.0
++    # via
++    #   outcome
++    #   trio
+ exceptiongroup==1.1.1
+-    # via pytest
++    # via
++    #   pytest
++    #   trio
++idna==3.6
++    # via trio
+ iniconfig==2.0.0
+     # via pytest
++outcome==1.3.0.post0
++    # via trio
+ packaging==23.1
+     # via pytest
+ pluggy==1.2.0
+     # via pytest
+ pytest==7.4.0
+     # via -r requirements/tests.in
++sniffio==1.3.1
++    # via trio
++sortedcontainers==2.4.0
++    # via trio
+ tomli==2.0.1
+     # via pytest
++trio==0.22.2
++    # via -r requirements/tests.in
+diff --git a/src/jinja2/async_utils.py b/src/jinja2/async_utils.py
+index 715d701..dca965e 100644
+--- a/src/jinja2/async_utils.py
++++ b/src/jinja2/async_utils.py
+@@ -6,6 +6,9 @@ from functools import wraps
+ from .utils import _PassArg
+ from .utils import pass_eval_context
+ 
++if t.TYPE_CHECKING:
++    import typing_extensions as te
++
+ V = t.TypeVar("V")
+ 
+ 
+@@ -67,15 +70,27 @@ async def auto_await(value: t.Union[t.Awaitable["V"], "V"]) -> "V":
+     return t.cast("V", value)
+ 
+ 
+-async def auto_aiter(
++class _IteratorToAsyncIterator(t.Generic[V]):
++    def __init__(self, iterator: "t.Iterator[V]"):
++        self._iterator = iterator
++
++    def __aiter__(self) -> "te.Self":
++        return self
++
++    async def __anext__(self) -> V:
++        try:
++            return next(self._iterator)
++        except StopIteration as e:
++            raise StopAsyncIteration(e.value) from e
++
++
++def auto_aiter(
+     iterable: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
+ ) -> "t.AsyncIterator[V]":
+     if hasattr(iterable, "__aiter__"):
+-        async for item in t.cast("t.AsyncIterable[V]", iterable):
+-            yield item
++        return iterable.__aiter__()
+     else:
+-        for item in iterable:
+-            yield item
++        return _IteratorToAsyncIterator(iter(iterable))
+ 
+ 
+ async def auto_to_list(
+diff --git a/src/jinja2/compiler.py b/src/jinja2/compiler.py
+index ff95c80..8972d1a 100644
+--- a/src/jinja2/compiler.py
++++ b/src/jinja2/compiler.py
+@@ -898,12 +898,15 @@ class CodeGenerator(NodeVisitor):
+             if not self.environment.is_async:
+                 self.writeline("yield from parent_template.root_render_func(context)")
+             else:
+-                self.writeline(
+-                    "async for event in parent_template.root_render_func(context):"
+-                )
++                self.writeline("agen = parent_template.root_render_func(context)")
++                self.writeline("try:")
++                self.indent()
++                self.writeline("async for event in agen:")
+                 self.indent()
+                 self.writeline("yield event")
+                 self.outdent()
++                self.outdent()
++                self.writeline("finally: await agen.aclose()")
+             self.outdent(1 + (not self.has_known_extends))
+ 
+         # at this point we now have the blocks collected and can visit them too.
+@@ -973,14 +976,20 @@ class CodeGenerator(NodeVisitor):
+                 f"yield from context.blocks[{node.name!r}][0]({context})", node
+             )
+         else:
++            self.writeline(f"gen = context.blocks[{node.name!r}][0]({context})")
++            self.writeline("try:")
++            self.indent()
+             self.writeline(
+-                f"{self.choose_async()}for event in"
+-                f" context.blocks[{node.name!r}][0]({context}):",
++                f"{self.choose_async()}for event in gen:",
+                 node,
+             )
+             self.indent()
+             self.simple_write("event", frame)
+             self.outdent()
++            self.outdent()
++            self.writeline(
++                f"finally: {self.choose_async('await gen.aclose()', 'gen.close()')}"
++            )
+ 
+         self.outdent(level)
+ 
+@@ -1053,26 +1062,33 @@ class CodeGenerator(NodeVisitor):
+             self.writeline("else:")
+             self.indent()
+ 
+-        skip_event_yield = False
++        def loop_body() -> None:
++            self.indent()
++            self.simple_write("event", frame)
++            self.outdent()
++
+         if node.with_context:
+             self.writeline(
+-                f"{self.choose_async()}for event in template.root_render_func("
++                f"gen = template.root_render_func("
+                 "template.new_context(context.get_all(), True,"
+-                f" {self.dump_local_context(frame)})):"
++                f" {self.dump_local_context(frame)}))"
++            )
++            self.writeline("try:")
++            self.indent()
++            self.writeline(f"{self.choose_async()}for event in gen:")
++            loop_body()
++            self.outdent()
++            self.writeline(
++                f"finally: {self.choose_async('await gen.aclose()', 'gen.close()')}"
+             )
+         elif self.environment.is_async:
+             self.writeline(
+                 "for event in (await template._get_default_module_async())"
+                 "._body_stream:"
+             )
++            loop_body()
+         else:
+             self.writeline("yield from template._get_default_module()._body_stream")
+-            skip_event_yield = True
+-
+-        if not skip_event_yield:
+-            self.indent()
+-            self.simple_write("event", frame)
+-            self.outdent()
+ 
+         if node.ignore_missing:
+             self.outdent()
+diff --git a/src/jinja2/environment.py b/src/jinja2/environment.py
+index 185d332..d1b5bdf 100644
+--- a/src/jinja2/environment.py
++++ b/src/jinja2/environment.py
+@@ -1355,7 +1355,7 @@ class Template:
+ 
+     async def generate_async(
+         self, *args: t.Any, **kwargs: t.Any
+-    ) -> t.AsyncIterator[str]:
++    ) -> t.AsyncGenerator[str, object]:
+         """An async version of :meth:`generate`.  Works very similarly but
+         returns an async iterator instead.
+         """
+@@ -1367,8 +1367,14 @@ class Template:
+         ctx = self.new_context(dict(*args, **kwargs))
+ 
+         try:
+-            async for event in self.root_render_func(ctx):  # type: ignore
+-                yield event
++            agen = self.root_render_func(ctx)
++            try:
++                async for event in agen:  # type: ignore
++                    yield event
++            finally:
++                # we can't use async with aclosing(...) because that's only
++                # in 3.10+
++                await agen.aclose()  # type: ignore
+         except Exception:
+             yield self.environment.handle_exception()
+ 
+diff --git a/tests/test_async.py b/tests/test_async.py
+index c9ba70c..4edced9 100644
+--- a/tests/test_async.py
++++ b/tests/test_async.py
+@@ -1,6 +1,7 @@
+ import asyncio
+ 
+ import pytest
++import trio
+ 
+ from jinja2 import ChainableUndefined
+ from jinja2 import DictLoader
+@@ -13,7 +14,16 @@ from jinja2.exceptions import UndefinedError
+ from jinja2.nativetypes import NativeEnvironment
+ 
+ 
+-def test_basic_async():
++def _asyncio_run(async_fn, *args):
++    return asyncio.run(async_fn(*args))
++
++
++@pytest.fixture(params=[_asyncio_run, trio.run], ids=["asyncio", "trio"])
++def run_async_fn(request):
++    return request.param
++
++
++def test_basic_async(run_async_fn):
+     t = Template(
+         "{% for item in [1, 2, 3] %}[{{ item }}]{% endfor %}", enable_async=True
+     )
+@@ -21,11 +31,11 @@ def test_basic_async():
+     async def func():
+         return await t.render_async()
+ 
+-    rv = asyncio.run(func())
++    rv = run_async_fn(func)
+     assert rv == "[1][2][3]"
+ 
+ 
+-def test_await_on_calls():
++def test_await_on_calls(run_async_fn):
+     t = Template("{{ async_func() + normal_func() }}", enable_async=True)
+ 
+     async def async_func():
+@@ -37,7 +47,7 @@ def test_await_on_calls():
+     async def func():
+         return await t.render_async(async_func=async_func, normal_func=normal_func)
+ 
+-    rv = asyncio.run(func())
++    rv = run_async_fn(func)
+     assert rv == "65"
+ 
+ 
+@@ -54,7 +64,7 @@ def test_await_on_calls_normal_render():
+     assert rv == "65"
+ 
+ 
+-def test_await_and_macros():
++def test_await_and_macros(run_async_fn):
+     t = Template(
+         "{% macro foo(x) %}[{{ x }}][{{ async_func() }}]{% endmacro %}{{ foo(42) }}",
+         enable_async=True,
+@@ -66,11 +76,11 @@ def test_await_and_macros():
+     async def func():
+         return await t.render_async(async_func=async_func)
+ 
+-    rv = asyncio.run(func())
++    rv = run_async_fn(func)
+     assert rv == "[42][42]"
+ 
+ 
+-def test_async_blocks():
++def test_async_blocks(run_async_fn):
+     t = Template(
+         "{% block foo %}<Test>{% endblock %}{{ self.foo() }}",
+         enable_async=True,
+@@ -80,7 +90,7 @@ def test_async_blocks():
+     async def func():
+         return await t.render_async()
+ 
+-    rv = asyncio.run(func())
++    rv = run_async_fn(func)
+     assert rv == "<Test><Test>"
+ 
+ 
+@@ -156,8 +166,8 @@ class TestAsyncImports:
+         test_env_async.from_string('{% from "foo" import bar, with, context %}')
+         test_env_async.from_string('{% from "foo" import bar, with with context %}')
+ 
+-    def test_exports(self, test_env_async):
+-        coro = test_env_async.from_string(
++    def test_exports(self, test_env_async, run_async_fn):
++        coro_fn = test_env_async.from_string(
+             """
+             {% macro toplevel() %}...{% endmacro %}
+             {% macro __private() %}...{% endmacro %}
+@@ -166,9 +176,9 @@ class TestAsyncImports:
+                 {% macro notthere() %}{% endmacro %}
+             {% endfor %}
+             """
+-        )._get_default_module_async()
+-        m = asyncio.run(coro)
+-        assert asyncio.run(m.toplevel()) == "..."
++        )._get_default_module_async
++        m = run_async_fn(coro_fn)
++        assert run_async_fn(m.toplevel) == "..."
+         assert not hasattr(m, "__missing")
+         assert m.variable == 42
+         assert not hasattr(m, "notthere")
+@@ -457,17 +467,19 @@ class TestAsyncForLoop:
+         )
+         assert tmpl.render(items=reversed([3, 2, 1])) == "1,2,3"
+ 
+-    def test_loop_errors(self, test_env_async):
++    def test_loop_errors(self, test_env_async, run_async_fn):
+         tmpl = test_env_async.from_string(
+             """{% for item in [1] if loop.index
+                                       == 0 %}...{% endfor %}"""
+         )
+-        pytest.raises(UndefinedError, tmpl.render)
++        with pytest.raises(UndefinedError):
++            run_async_fn(tmpl.render_async)
++
+         tmpl = test_env_async.from_string(
+             """{% for item in [] %}...{% else
+             %}{{ loop }}{% endfor %}"""
+         )
+-        assert tmpl.render() == ""
++        assert run_async_fn(tmpl.render_async) == ""
+ 
+     def test_loop_filter(self, test_env_async):
+         tmpl = test_env_async.from_string(
+@@ -597,7 +609,7 @@ class TestAsyncForLoop:
+         assert t.render(a=dict(b=[1, 2, 3])) == "1"
+ 
+ 
+-def test_namespace_awaitable(test_env_async):
++def test_namespace_awaitable(test_env_async, run_async_fn):
+     async def _test():
+         t = test_env_async.from_string(
+             '{% set ns = namespace(foo="Bar") %}{{ ns.foo }}'
+@@ -605,10 +617,10 @@ def test_namespace_awaitable(test_env_async):
+         actual = await t.render_async()
+         assert actual == "Bar"
+ 
+-    asyncio.run(_test())
++    run_async_fn(_test)
+ 
+ 
+-def test_chainable_undefined_aiter():
++def test_chainable_undefined_aiter(run_async_fn):
+     async def _test():
+         t = Template(
+             "{% for x in a['b']['c'] %}{{ x }}{% endfor %}",
+@@ -618,7 +630,7 @@ def test_chainable_undefined_aiter():
+         rv = await t.render_async(a={})
+         assert rv == ""
+ 
+-    asyncio.run(_test())
++    run_async_fn(_test)
+ 
+ 
+ @pytest.fixture
+@@ -626,22 +638,22 @@ def async_native_env():
+     return NativeEnvironment(enable_async=True)
+ 
+ 
+-def test_native_async(async_native_env):
++def test_native_async(async_native_env, run_async_fn):
+     async def _test():
+         t = async_native_env.from_string("{{ x }}")
+         rv = await t.render_async(x=23)
+         assert rv == 23
+ 
+-    asyncio.run(_test())
++    run_async_fn(_test)
+ 
+ 
+-def test_native_list_async(async_native_env):
++def test_native_list_async(async_native_env, run_async_fn):
+     async def _test():
+         t = async_native_env.from_string("{{ x }}")
+         rv = await t.render_async(x=list(range(3)))
+         assert rv == [0, 1, 2]
+ 
+-    asyncio.run(_test())
++    run_async_fn(_test)
+ 
+ 
+ def test_getitem_after_filter():
+@@ -658,3 +670,65 @@ def test_getitem_after_call():
+     t = env.from_string("{{ add_each(a, 2)[1:] }}")
+     out = t.render(a=range(3))
+     assert out == "[3, 4]"
++
++
++def test_basic_generate_async(run_async_fn):
++    t = Template(
++        "{% for item in [1, 2, 3] %}[{{ item }}]{% endfor %}", enable_async=True
++    )
++
++    async def func():
++        agen = t.generate_async()
++        try:
++            return await agen.__anext__()
++        finally:
++            await agen.aclose()
++
++    rv = run_async_fn(func)
++    assert rv == "["
++
++
++def test_include_generate_async(run_async_fn, test_env_async):
++    t = test_env_async.from_string('{% include "header" %}')
++
++    async def func():
++        agen = t.generate_async()
++        try:
++            return await agen.__anext__()
++        finally:
++            await agen.aclose()
++
++    rv = run_async_fn(func)
++    assert rv == "["
++
++
++def test_blocks_generate_async(run_async_fn):
++    t = Template(
++        "{% block foo %}<Test>{% endblock %}{{ self.foo() }}",
++        enable_async=True,
++        autoescape=True,
++    )
++
++    async def func():
++        agen = t.generate_async()
++        try:
++            return await agen.__anext__()
++        finally:
++            await agen.aclose()
++
++    rv = run_async_fn(func)
++    assert rv == "<Test>"
++
++
++def test_async_extend(run_async_fn, test_env_async):
++    t = test_env_async.from_string('{% extends "header" %}')
++
++    async def func():
++        agen = t.generate_async()
++        try:
++            return await agen.__anext__()
++        finally:
++            await agen.aclose()
++
++    rv = run_async_fn(func)
++    assert rv == "["
+diff --git a/tests/test_async_filters.py b/tests/test_async_filters.py
+index f5b2627..e8cc350 100644
+--- a/tests/test_async_filters.py
++++ b/tests/test_async_filters.py
+@@ -1,6 +1,9 @@
++import asyncio
++import contextlib
+ from collections import namedtuple
+ 
+ import pytest
++import trio
+ from markupsafe import Markup
+ 
+ from jinja2 import Environment
+@@ -26,10 +29,39 @@ def env_async():
+     return Environment(enable_async=True)
+ 
+ 
++def _asyncio_run(async_fn, *args):
++    return asyncio.run(async_fn(*args))
++
++
++@pytest.fixture(params=[_asyncio_run, trio.run], ids=["asyncio", "trio"])
++def run_async_fn(request):
++    return request.param
++
++
++@contextlib.asynccontextmanager
++async def closing_factory():
++    async with contextlib.AsyncExitStack() as stack:
++
++        def closing(maybe_agen):
++            try:
++                aclose = maybe_agen.aclose
++            except AttributeError:
++                pass
++            else:
++                stack.push_async_callback(aclose)
++            return maybe_agen
++
++        yield closing
++
++
+ @mark_dualiter("foo", lambda: range(10))
+-def test_first(env_async, foo):
+-    tmpl = env_async.from_string("{{ foo()|first }}")
+-    out = tmpl.render(foo=foo)
++def test_first(env_async, foo, run_async_fn):
++    async def test():
++        async with closing_factory() as closing:
++            tmpl = env_async.from_string("{{ closing(foo())|first }}")
++            return await tmpl.render_async(foo=foo, closing=closing)
++
++    out = run_async_fn(test)
+     assert out == "0"
+ 
+ 
+@@ -245,18 +277,23 @@ def test_slice(env_async, items):
+     )
+ 
+ 
+-def test_custom_async_filter(env_async):
++def test_custom_async_filter(env_async, run_async_fn):
+     async def customfilter(val):
+         return str(val)
+ 
+-    env_async.filters["customfilter"] = customfilter
+-    tmpl = env_async.from_string("{{ 'static'|customfilter }} {{ arg|customfilter }}")
+-    out = tmpl.render(arg="dynamic")
++    async def test():
++        env_async.filters["customfilter"] = customfilter
++        tmpl = env_async.from_string(
++            "{{ 'static'|customfilter }} {{ arg|customfilter }}"
++        )
++        return await tmpl.render_async(arg="dynamic")
++
++    out = run_async_fn(test)
+     assert out == "static dynamic"
+ 
+ 
+ @mark_dualiter("items", lambda: range(10))
+-def test_custom_async_iteratable_filter(env_async, items):
++def test_custom_async_iteratable_filter(env_async, items, run_async_fn):
+     async def customfilter(iterable):
+         items = []
+         async for item in auto_aiter(iterable):
+@@ -265,9 +302,13 @@ def test_custom_async_iteratable_filter(env_async, items):
+                 break
+         return ",".join(items)
+ 
+-    env_async.filters["customfilter"] = customfilter
+-    tmpl = env_async.from_string(
+-        "{{ items()|customfilter }} .. {{ [3, 4, 5, 6]|customfilter }}"
+-    )
+-    out = tmpl.render(items=items)
++    async def test():
++        async with closing_factory() as closing:
++            env_async.filters["customfilter"] = customfilter
++            tmpl = env_async.from_string(
++                "{{ closing(items())|customfilter }} .. {{ [3, 4, 5, 6]|customfilter }}"
++            )
++            return await tmpl.render_async(items=items, closing=closing)
++
++    out = run_async_fn(test)
+     assert out == "0,1,2 .. 3,4,5"
+-- 
+2.30.2
+
diff -Nru jinja2-3.1.3/debian/patches/0002-fix-test_package_zip_list-on-3.13.patch jinja2-3.1.3/debian/patches/0002-fix-test_package_zip_list-on-3.13.patch
--- jinja2-3.1.3/debian/patches/0002-fix-test_package_zip_list-on-3.13.patch	1970-01-01 02:00:00.000000000 +0200
+++ jinja2-3.1.3/debian/patches/0002-fix-test_package_zip_list-on-3.13.patch	2024-12-08 07:58:50.000000000 +0200
@@ -0,0 +1,69 @@
+From 873bc260f462219619fe8011b73f592b7f035783 Mon Sep 17 00:00:00 2001
+From: Thomas Grainger <tagr...@gmail.com>
+Date: Mon, 13 May 2024 18:02:35 +0100
+Subject: fix test_package_zip_list on 3.13
+
+---
+ src/jinja2/loaders.py | 32 ++++++++++++++++++++++++++------
+ 1 file changed, 26 insertions(+), 6 deletions(-)
+
+diff --git a/src/jinja2/loaders.py b/src/jinja2/loaders.py
+index 32f3a74..cfac178 100644
+--- a/src/jinja2/loaders.py
++++ b/src/jinja2/loaders.py
+@@ -235,6 +235,30 @@ class FileSystemLoader(BaseLoader):
+         return sorted(found)
+ 
+ 
++if sys.version_info >= (3, 13):
++
++    def _get_zipimporter_files(z: t.Any) -> t.Dict[str, object]:
++        try:
++            get_files = z._get_files
++        except AttributeError as e:
++            raise TypeError(
++                "This zip import does not have the required"
++                " metadata to list templates."
++            ) from e
++        return get_files()
++else:
++
++    def _get_zipimporter_files(z: t.Any) -> t.Dict[str, object]:
++        try:
++            files = z._files
++        except AttributeError as e:
++            raise TypeError(
++                "This zip import does not have the required"
++                " metadata to list templates."
++            ) from e
++        return files  # type: ignore[no-any-return]
++
++
+ class PackageLoader(BaseLoader):
+     """Load templates from a directory in a Python package.
+ 
+@@ -379,11 +403,7 @@ class PackageLoader(BaseLoader):
+                     for name in filenames
+                 )
+         else:
+-            if not hasattr(self._loader, "_files"):
+-                raise TypeError(
+-                    "This zip import does not have the required"
+-                    " metadata to list templates."
+-                )
++            files = _get_zipimporter_files(self._loader)
+ 
+             # Package is a zip file.
+             prefix = (
+@@ -392,7 +412,7 @@ class PackageLoader(BaseLoader):
+             )
+             offset = len(prefix)
+ 
+-            for name in self._loader._files.keys():
++            for name in files:
+                 # Find names under the templates directory that aren't directories.
+                 if name.startswith(prefix) and name[-1] != os.path.sep:
+                     results.append(name[offset:].replace(os.path.sep, "/"))
+-- 
+2.30.2
+
diff -Nru jinja2-3.1.3/debian/patches/0003-disallow-invalid-characters-in-keys-to-xmlattr-filte.patch jinja2-3.1.3/debian/patches/0003-disallow-invalid-characters-in-keys-to-xmlattr-filte.patch
--- jinja2-3.1.3/debian/patches/0003-disallow-invalid-characters-in-keys-to-xmlattr-filte.patch	1970-01-01 02:00:00.000000000 +0200
+++ jinja2-3.1.3/debian/patches/0003-disallow-invalid-characters-in-keys-to-xmlattr-filte.patch	2024-12-08 07:58:50.000000000 +0200
@@ -0,0 +1,89 @@
+From 91542fbfcde3bd16e414e4270b338e36f6183f64 Mon Sep 17 00:00:00 2001
+From: David Lord <david...@gmail.com>
+Date: Thu, 2 May 2024 09:14:00 -0700
+Subject: disallow invalid characters in keys to xmlattr filter
+
+---
+ src/jinja2/filters.py | 22 +++++++++++++++++-----
+ tests/test_filters.py | 11 ++++++-----
+ 2 files changed, 23 insertions(+), 10 deletions(-)
+
+diff --git a/src/jinja2/filters.py b/src/jinja2/filters.py
+index c7ecc9b..bdf6f22 100644
+--- a/src/jinja2/filters.py
++++ b/src/jinja2/filters.py
+@@ -248,7 +248,9 @@ def do_items(value: t.Union[t.Mapping[K, V], Undefined]) -> t.Iterator[t.Tuple[K
+     yield from value.items()
+ 
+ 
+-_space_re = re.compile(r"\s", flags=re.ASCII)
++# Check for characters that would move the parser state from key to value.
++# https://html.spec.whatwg.org/#attribute-name-state
++_attr_key_re = re.compile(r"[\s/>=]", flags=re.ASCII)
+ 
+ 
+ @pass_eval_context
+@@ -257,8 +259,14 @@ def do_xmlattr(
+ ) -> str:
+     """Create an SGML/XML attribute string based on the items in a dict.
+ 
+-    If any key contains a space, this fails with a ``ValueError``. Values that
+-    are neither ``none`` nor ``undefined`` are automatically escaped.
++    **Values** that are neither ``none`` nor ``undefined`` are automatically
++    escaped, safely allowing untrusted user input.
++
++    User input should not be used as **keys** to this filter. If any key
++    contains a space, ``/`` solidus, ``>`` greater-than sign, or ``=`` equals
++    sign, this fails with a ``ValueError``. Regardless of this, user input
++    should never be used as keys to this filter, or must be separately validated
++    first.
+ 
+     .. sourcecode:: html+jinja
+ 
+@@ -278,6 +286,10 @@ def do_xmlattr(
+     As you can see it automatically prepends a space in front of the item
+     if the filter returned something unless the second parameter is false.
+ 
++    .. versionchanged:: 3.1.4
++        Keys with ``/`` solidus, ``>`` greater-than sign, or ``=`` equals sign
++        are not allowed.
++
+     .. versionchanged:: 3.1.3
+         Keys with spaces are not allowed.
+     """
+@@ -287,8 +299,8 @@ def do_xmlattr(
+         if value is None or isinstance(value, Undefined):
+             continue
+ 
+-        if _space_re.search(key) is not None:
+-            raise ValueError(f"Spaces are not allowed in attributes: '{key}'")
++        if _attr_key_re.search(key) is not None:
++            raise ValueError(f"Invalid character in attribute name: {key!r}")
+ 
+         items.append(f'{escape(key)}="{escape(value)}"')
+ 
+diff --git a/tests/test_filters.py b/tests/test_filters.py
+index f50ed13..d8e9114 100644
+--- a/tests/test_filters.py
++++ b/tests/test_filters.py
+@@ -474,11 +474,12 @@ class TestFilter:
+         assert 'bar="23"' in out
+         assert 'blub:blub="&lt;?&gt;"' in out
+ 
+-    def test_xmlattr_key_with_spaces(self, env):
+-        with pytest.raises(ValueError, match="Spaces are not allowed"):
+-            env.from_string(
+-                "{{ {'src=1 onerror=alert(1)': 'my_class'}|xmlattr }}"
+-            ).render()
++    @pytest.mark.parametrize("sep", ("\t", "\n", "\f", " ", "/", ">", "="))
++    def test_xmlattr_key_invalid(self, env: Environment, sep: str) -> None:
++        with pytest.raises(ValueError, match="Invalid character"):
++            env.from_string("{{ {key: 'my_class'}|xmlattr }}").render(
++                key=f"class{sep}onclick=alert(1)"
++            )
+ 
+     def test_sort1(self, env):
+         tmpl = env.from_string("{{ [2, 3, 1]|sort }}|{{ [2, 3, 1]|sort(true) }}")
+-- 
+2.30.2
+
diff -Nru jinja2-3.1.3/debian/patches/series jinja2-3.1.3/debian/patches/series
--- jinja2-3.1.3/debian/patches/series	2024-03-05 10:30:31.000000000 +0200
+++ jinja2-3.1.3/debian/patches/series	2024-12-08 07:59:01.000000000 +0200
@@ -1,2 +1,5 @@
 py3.9-fix-collections-import.patch
 0002-docs-disable-sphinxcontrib.log_cabinet.patch
+0001-test-on-trio-fix-all-missing-aclose-related-warnings.patch
+0002-fix-test_package_zip_list-on-3.13.patch
+0003-disallow-invalid-characters-in-keys-to-xmlattr-filte.patch

Reply via email to