Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package elixir for openSUSE:Factory checked in at 2026-08-29 17:43:56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/elixir (Old) and /work/SRC/openSUSE:Factory/.elixir.new.1265 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "elixir" Sat Aug 29 17:43:56 2026 rev:51 rq:1374609 version:1.20.4 Changes: -------- --- /work/SRC/openSUSE:Factory/elixir/elixir.changes 2026-08-05 17:50:12.621659869 +0200 +++ /work/SRC/openSUSE:Factory/.elixir.new.1265/elixir.changes 2026-08-29 17:44:57.942571014 +0200 @@ -1,0 +2,24 @@ +Sat Aug 29 14:38:29 UTC 2026 - Alessio Biancalana <[email protected]> + +- Add 005-skip-mix-compile-concurrent-listening-test.patch: skip the + Mix.Tasks.CompileTest "listening to concurrent compilations" test, + which hangs in the OBS build environment + +------------------------------------------------------------------- +Sat Aug 29 08:46:53 UTC 2026 - Alessio Biancalana <[email protected]> + +- Upgrade to Elixir 1.20.4: + * Changelog available at https://hexdocs.pm/elixir/1.20.4/changelog.html + +------------------------------------------------------------------- +Fri Aug 21 04:55:59 UTC 2026 - Simon Lees <[email protected]> + +- Add 004-skip-mix-sync-lock-on-taken-test.patch: skip the + Mix.Sync.Lock "calls :on_taken when the lock is held by a + different process" test, new in Elixir 1.18. It probes lock + holders over a loopback TCP socket, which does not work in the + network-isolated OBS build root and hangs until the + assert_receive timeout, failing the build. Noticed on AARCH64 in + SLES + +------------------------------------------------------------------- Old: ---- elixir-1.20.3-doc.zip elixir-1.20.3.tar.gz New: ---- 004-skip-mix-sync-lock-on-taken-test.patch 005-skip-mix-compile-concurrent-listening-test.patch elixir-1.20.4-doc.zip elixir-1.20.4.tar.gz ----------(New B)---------- New: - Add 004-skip-mix-sync-lock-on-taken-test.patch: skip the Mix.Sync.Lock "calls :on_taken when the lock is held by a New: - Add 005-skip-mix-compile-concurrent-listening-test.patch: skip the Mix.Tasks.CompileTest "listening to concurrent compilations" test, ----------(New E)---------- ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ elixir.spec ++++++ --- /var/tmp/diff_new_pack.2m6Twl/_old 2026-08-29 17:44:58.742599313 +0200 +++ /var/tmp/diff_new_pack.2m6Twl/_new 2026-08-29 17:44:58.744599383 +0200 @@ -18,7 +18,7 @@ %define elixirdir %{_prefix}/lib/elixir Name: elixir -Version: 1.20.3 +Version: 1.20.4 Release: 0 Summary: Functional meta-programming aware language built atop Erlang License: Apache-2.0 @@ -31,6 +31,8 @@ Patch0: 001-skip-translator-supervisor-test.patch Patch1: 002-skip-tests-iex-helpers.patch Patch2: 003-skip-tests-daemon-mode-source.patch +Patch3: 004-skip-mix-sync-lock-on-taken-test.patch +Patch4: 005-skip-mix-compile-concurrent-listening-test.patch BuildRequires: erlang >= 25 BuildRequires: erlang-dialyzer BuildRequires: erlang-src @@ -102,7 +104,7 @@ %check export LANG=en_US.UTF-8 -%make_build test +/usr/bin/make -O V=1 VERBOSE=1 test %files %doc CHANGELOG.md README.md ++++++ 004-skip-mix-sync-lock-on-taken-test.patch ++++++ Index: elixir-1.18.1/lib/mix/test/mix/sync/lock_test.exs =================================================================== --- elixir-1.18.1.orig/lib/mix/test/mix/sync/lock_test.exs +++ elixir-1.18.1/lib/mix/test/mix/sync/lock_test.exs @@ -153,6 +153,11 @@ defmodule Mix.Sync.LockTest do end) == :still_works! end + # Mix.Sync.Lock probes other lock holders over a loopback TCP socket. + # The OBS build root has no usable network access, so the probe + # connect always fails and :on_taken is never invoked, hanging + # this test until the assert_receive timeout. + @tag :skip test "calls :on_taken when the lock is held by a different process" do parent = self() ++++++ 005-skip-mix-compile-concurrent-listening-test.patch ++++++ diff --git a/lib/mix/test/mix/tasks/compile_test.exs b/lib/mix/test/mix/tasks/compile_test.exs index d202102..743f8d2 100644 --- a/lib/mix/test/mix/tasks/compile_test.exs +++ b/lib/mix/test/mix/tasks/compile_test.exs @@ -389,6 +389,7 @@ defmodule Mix.Tasks.CompileTest do end) end + @tag :skip test "listening to concurrent compilations" do timeout = 2_000 ++++++ _scmsync.obsinfo ++++++ --- /var/tmp/diff_new_pack.2m6Twl/_old 2026-08-29 17:44:58.853603239 +0200 +++ /var/tmp/diff_new_pack.2m6Twl/_new 2026-08-29 17:44:58.856603345 +0200 @@ -1,6 +1,6 @@ -mtime: 1785864304 -commit: c98851904b7aba54535399b4a316f798e58292e7abc79600154d6d4138afac9d +mtime: 1788014578 +commit: bbf3086d78dde159067fb606ba70d13c01bcd172ee420ae2d7223d1b33d320ff url: https://src.opensuse.org/erlang/elixir -revision: c98851904b7aba54535399b4a316f798e58292e7abc79600154d6d4138afac9d +revision: bbf3086d78dde159067fb606ba70d13c01bcd172ee420ae2d7223d1b33d320ff projectscmsync: https://src.opensuse.org/erlang/_ObsPrj.git ++++++ build.specials.obscpio ++++++ ++++++ build.specials.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/.gitignore new/.gitignore --- old/.gitignore 1970-01-01 01:00:00.000000000 +0100 +++ new/.gitignore 2026-08-29 16:42:58.000000000 +0200 @@ -0,0 +1 @@ +.osc ++++++ elixir-1.20.3-doc.zip -> elixir-1.20.4-doc.zip ++++++ /work/SRC/openSUSE:Factory/elixir/elixir-1.20.3-doc.zip /work/SRC/openSUSE:Factory/.elixir.new.1265/elixir-1.20.4-doc.zip differ: char 11, line 1 ++++++ elixir-1.20.3.tar.gz -> elixir-1.20.4.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.3/CHANGELOG.md new/elixir-1.20.4/CHANGELOG.md --- old/elixir-1.20.3/CHANGELOG.md 2026-08-04 16:31:51.000000000 +0200 +++ new/elixir-1.20.4/CHANGELOG.md 2026-08-28 12:02:55.000000000 +0200 @@ -192,6 +192,22 @@ You can enable it by setting `elixirc_options: [module_definition: :interpreted]` in your `mix.exs`. +## v1.20.4 (2026-08-28) + +### 1. Security + + * [List] Avoid recursion when invalid charlists are given to `List.to_string/1` or `List.to_charlist/1` (CVE-2026-75758, GHSA-jf5q-v438-665c) + +### 2. Bug fixes + +#### Elixir + + * [Base] Fix false positives in Base64 validation + * [Kernel] Fix inferred type of `:erlang.split_binary/2` when given a bitstring + * [Kernel] Fix type checker crash on comprehensions with a non-returning `:into` expression + * [String] Fix final sigma handling in Greek casing + * [Task] Fix `Task.yield_many/2` waiting indefinitely when `:limit` exceeds the task count + ## v1.20.3 (2026-08-04) ### 1. Enhancements @@ -206,7 +222,7 @@ * [Kernel] Improve type precision of the `++/2` operator * [Kernel] Improve type precision of the `send/2` return type - * [Kernel] Improve type refinement for `:erlang.andalso/2` and `:erlang.orelse/2` + * [Kernel] Improve type refinement for `and/2` and `or/2` * [Kernel] Fix type checking of `Map.put/3` on empty map types * [Kernel] Fix type checking of tuple insertion on equivalent tuple types * [Kernel] Fix type checking of tuple insertion on gradual types with empty static parts diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.3/VERSION new/elixir-1.20.4/VERSION --- old/elixir-1.20.3/VERSION 2026-08-04 16:31:51.000000000 +0200 +++ new/elixir-1.20.4/VERSION 2026-08-28 12:02:55.000000000 +0200 @@ -1 +1 @@ -1.20.3 +1.20.4 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.3/bin/elixir new/elixir-1.20.4/bin/elixir --- old/elixir-1.20.3/bin/elixir 2026-08-04 16:31:51.000000000 +0200 +++ new/elixir-1.20.4/bin/elixir 2026-08-28 12:02:55.000000000 +0200 @@ -6,7 +6,7 @@ set -e -ELIXIR_VERSION=1.20.3 +ELIXIR_VERSION=1.20.4 if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then cat <<USAGE >&2 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.3/bin/elixir.bat new/elixir-1.20.4/bin/elixir.bat --- old/elixir-1.20.3/bin/elixir.bat 2026-08-04 16:31:51.000000000 +0200 +++ new/elixir-1.20.4/bin/elixir.bat 2026-08-28 12:02:55.000000000 +0200 @@ -4,7 +4,7 @@ :: SPDX-FileCopyrightText: 2021 The Elixir Team :: SPDX-FileCopyrightText: 2012 Plataformatec -set ELIXIR_VERSION=1.20.3 +set ELIXIR_VERSION=1.20.4 if ""%1""=="""" if ""%2""=="""" goto documentation if /I ""%1""==""--help"" if ""%2""=="""" goto documentation diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.3/lib/elixir/lib/base.ex new/elixir-1.20.4/lib/elixir/lib/base.ex --- old/elixir-1.20.3/lib/elixir/lib/base.ex 2026-08-04 16:31:51.000000000 +0200 +++ new/elixir-1.20.4/lib/elixir/lib/base.ex 2026-08-28 12:02:55.000000000 +0200 @@ -187,21 +187,21 @@ # For base64 standard, '/' (0x2F) sits exactly one below '0' (0x30), so we # extend the digit range to [0x2F, 0x39], which absorbs '/' into one range - # check — saves one Mycroft singleton. Trick lifted from + # check — saves one singleton term. Trick lifted from # https://lemire.me/blog/2025/04/13/detect-control-characters-quotes-and-backslashes-efficiently-using-swar/ @swar_ge_slash 0x51515151515151 - # Mycroft zero-byte detection for base64 singletons (+, -, _). - # Per lane: high bit set iff `bxor(w, K*ones) - 0x01..01` has its high bit - # set, i.e. that byte's V value was 0 → original byte was K. Simplified - # (no `bnot V` term) — for ASCII-gated `w`, borrow-propagation false - # positives only occur for adjacent bytes that happen to equal `K xor 0x01`, - # which is outside the base64 alphabet, so it never matters here. - # Pattern follows https://github.com/elixir-lang/elixir/pull/15255. - @swar_mask01 0x01010101010101 - @swar_plus_x7 0x2B2B2B2B2B2B2B - @swar_dash_x7 0x2D2D2D2D2D2D2D - @swar_under_x7 0x5F5F5F5F5F5F5F + # base64's '+', '-' and '_' border no other range, so each is checked as the + # degenerate range [c, c]. Mycroft zero-byte detection is one operation + # cheaper per singleton but not exact per lane: subtracting 0x01..01 makes a + # zero lane borrow from the lane above it, and a lane holding 1 turns that + # borrow into 0xFF, so bytes equal to `c bxor 0x01` read as matches too. + @swar_ge_plus 0x55555555555555 + @swar_gt_plus 0x54545454545454 + @swar_ge_dash 0x53535353535353 + @swar_gt_dash 0x52525252525252 + @swar_ge_underscore 0x21212121212121 + @swar_gt_underscore 0x20202020202020 # Per-byte validity checks (used in both the SWAR clauses for the 8th byte # of each stride and in the body of the sub-8-byte tail clauses). @@ -365,8 +365,8 @@ # base64 SWAR word validity: 3 ranges (A-Z, a-z, 0-9) OR'd with singletons. # For base, the digit range is extended to [0x2F, 0x39] to absorb '/' as - # part of one range (Lemire merge), leaving only '+' as a Mycroft singleton. - # For url, the singletons '-' and '_' are detected via two Mycroft terms. + # part of one range (Lemire merge), leaving only '+' as a singleton. For url, + # '-' and '_' are two more singletons. defp valid_word64base?(w) do band(w, @swar_mask80) == 0 and band( @@ -378,7 +378,7 @@ ), bxor(w + @swar_ge_slash, w + @swar_gt_9) ), - bxor(w, @swar_plus_x7) - @swar_mask01 + bxor(w + @swar_ge_plus, w + @swar_gt_plus) ), @swar_mask80 ) == @swar_mask80 @@ -396,8 +396,8 @@ bxor(w + @swar_ge_0, w + @swar_gt_9) ), bor( - bxor(w, @swar_dash_x7) - @swar_mask01, - bxor(w, @swar_under_x7) - @swar_mask01 + bxor(w + @swar_ge_dash, w + @swar_gt_dash), + bxor(w + @swar_ge_underscore, w + @swar_gt_underscore) ) ), @swar_mask80 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.3/lib/elixir/lib/inspect/algebra.ex new/elixir-1.20.4/lib/elixir/lib/inspect/algebra.ex --- old/elixir-1.20.3/lib/elixir/lib/inspect/algebra.ex 2026-08-04 16:31:51.000000000 +0200 +++ new/elixir-1.20.4/lib/elixir/lib/inspect/algebra.ex 2026-08-28 12:02:55.000000000 +0200 @@ -21,8 +21,8 @@ is `:decimal` and if it is printable, otherwise in bit syntax. See `String.printable?/1` to learn when a string is printable. - * `:charlists` - when `:as_charlists` all lists will be printed as charlists, - non-printable elements will be escaped. + * `:charlists` - when `:as_charlists` all charlists will be printed as charlists, + non-printable code points will be escaped. Other lists will be printed as lists. When `:as_lists` all lists will be printed as lists. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.3/lib/elixir/lib/inspect.ex new/elixir-1.20.4/lib/elixir/lib/inspect.ex --- old/elixir-1.20.3/lib/elixir/lib/inspect.ex 2026-08-04 16:31:51.000000000 +0200 +++ new/elixir-1.20.4/lib/elixir/lib/inspect.ex 2026-08-28 12:02:55.000000000 +0200 @@ -387,13 +387,20 @@ close = color_doc("]", :list, opts) cond do - lists == :as_charlists or (lists == :infer and List.ascii_printable?(term, printable_limit)) -> - inspected = - case Identifier.escape(IO.chardata_to_string(term), ?", printable_limit) do - {escaped, ""} -> [?~, ?c, ?", escaped, ?"] - {escaped, _} -> [?~, ?c, ?", escaped, ?", " ++ ..."] + (lists == :as_charlists and unicode_list?(term, printable_limit)) or + (lists == :infer and List.ascii_printable?(term, printable_limit)) -> + {split, tail} = + if is_integer(printable_limit) do + case Enum.split(term, printable_limit) do + {split, []} -> {split, []} + {split, _} -> {split, " ++ ..."} + end + else + {term, []} end + {escaped, _} = Identifier.escape(IO.chardata_to_string(split), ?") + inspected = [?~, ?c, ?", escaped, ?" | tail] color_doc(IO.iodata_to_binary(inspected), :charlist, opts) keyword?(term) -> @@ -407,6 +414,19 @@ end end + defp unicode_list?(_, 0), do: true + + defp unicode_list?([char | rest], counter) + when char in 0..0xD7FF or char in 0xE000..0x10FFFF, + do: unicode_list?(rest, decrement(counter)) + + defp unicode_list?([], _counter), do: true + defp unicode_list?(_, _counter), do: false + + @compile {:inline, decrement: 1} + defp decrement(:infinity), do: :infinity + defp decrement(counter), do: counter - 1 + @doc false def keyword({key, value}, opts) do key = color_doc(Macro.inspect_atom(:key, key), :atom, opts) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.3/lib/elixir/lib/list.ex new/elixir-1.20.4/lib/elixir/lib/list.ex --- old/elixir-1.20.3/lib/elixir/lib/list.ex 2026-08-04 16:31:51.000000000 +0200 +++ new/elixir-1.20.4/lib/elixir/lib/list.ex 2026-08-28 12:02:55.000000000 +0200 @@ -1193,7 +1193,7 @@ Please check the given list or call inspect/1 to get the list representation, got: - #{inspect(list)} + #{inspect(list, charlists: :as_lists)} """ else result when is_binary(result) -> @@ -1245,7 +1245,7 @@ Please check the given list or call inspect/1 to get the list representation, got: - #{inspect(list)} + #{inspect(list, charlists: :as_lists)} """ else result when is_list(result) -> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.3/lib/elixir/lib/module/types/apply.ex new/elixir-1.20.4/lib/elixir/lib/module/types/apply.ex --- old/elixir-1.20.3/lib/elixir/lib/module/types/apply.ex 2026-08-04 16:31:51.000000000 +0200 +++ new/elixir-1.20.4/lib/elixir/lib/module/types/apply.ex 2026-08-28 12:02:55.000000000 +0200 @@ -213,7 +213,11 @@ {:erlang, :spawn_link, [{mfargs, pid()}]}, {:erlang, :spawn_monitor, [{[fun(0)], tuple([pid(), reference()])}]}, {:erlang, :spawn_monitor, [{mfargs, tuple([pid(), reference()])}]}, - {:erlang, :split_binary, [{[binary(), integer()], tuple([binary(), binary()])}]}, + {:erlang, :split_binary, + [ + {[binary(), integer()], tuple([binary(), binary()])}, + {[bitstring_no_binary(), integer()], tuple([binary(), bitstring_no_binary()])} + ]}, {:erlang, :tuple_size, [{[open_tuple([])], integer()}]}, {:erlang, :trunc, [{[union(integer(), float())], integer()}]}, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.3/lib/elixir/lib/module/types/expr.ex new/elixir-1.20.4/lib/elixir/lib/module/types/expr.ex --- old/elixir-1.20.3/lib/elixir/lib/module/types/expr.ex 2026-08-04 16:31:51.000000000 +0200 +++ new/elixir-1.20.4/lib/elixir/lib/module/types/expr.ex 2026-08-28 12:02:55.000000000 +0200 @@ -888,6 +888,9 @@ {true, false} -> {type, :bitstring, context} + + {false, false} -> + {type, :none, context} end else {_type, context} = diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.3/lib/elixir/lib/string.ex new/elixir-1.20.4/lib/elixir/lib/string.ex --- old/elixir-1.20.3/lib/elixir/lib/string.ex 2026-08-04 16:31:51.000000000 +0200 +++ new/elixir-1.20.4/lib/elixir/lib/string.ex 2026-08-28 12:02:55.000000000 +0200 @@ -1008,6 +1008,10 @@ IO.iodata_to_binary(downcase_ascii(string)) end + def downcase(string, :greek) when is_binary(string) do + String.Unicode.downcase_greek(string, []) + end + def downcase(string, mode) when is_binary(string) and mode in @conditional_mappings do String.Unicode.downcase(string, [], mode) end @@ -1058,19 +1062,39 @@ @letter_I_dot_above <<0x0130::utf8>> def capitalize(<<@letter_i, right::binary>>, mode) do - if(mode == :turkic, do: @letter_I_dot_above, else: @letter_I) <> downcase(right, mode) + capitalized = if mode == :turkic, do: @letter_I_dot_above, else: @letter_I + capitalized <> downcase_after_capitalize(right, mode, ?i) end def capitalize(string, mode) when is_binary(string) do case :unicode_util.gc(string) do - [gc] -> grapheme_to_binary(:string.titlecase([gc])) - [gc, rest] -> grapheme_to_binary(:string.titlecase([gc])) <> downcase(rest, mode) - [gc | rest] -> grapheme_to_binary(:string.titlecase([gc])) <> downcase(rest, mode) - [] -> "" - {:error, <<byte, rest::bits>>} -> <<byte>> <> downcase(rest, mode) + [gc] -> + grapheme_to_binary(:string.titlecase([gc])) + + [gc, rest] -> + grapheme_to_binary(:string.titlecase([gc])) <> + downcase_after_capitalize(rest, mode, gc) + + [gc | rest] -> + grapheme_to_binary(:string.titlecase([gc])) <> + downcase_after_capitalize(rest, mode, gc) + + [] -> + "" + + {:error, <<byte, rest::bits>>} -> + <<byte>> <> downcase(rest, mode) end end + defp downcase_after_capitalize(rest, :greek, gc) do + String.Unicode.downcase_greek(rest, [], grapheme_to_binary(gc)) + end + + defp downcase_after_capitalize(rest, mode, _gc) do + downcase(rest, mode) + end + @doc false @deprecated "Use String.trim_trailing/1 instead" defdelegate rstrip(binary), to: String.Break, as: :trim_trailing diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.3/lib/elixir/lib/task.ex new/elixir-1.20.4/lib/elixir/lib/task.ex --- old/elixir-1.20.3/lib/elixir/lib/task.ex 2026-08-04 16:31:51.000000000 +0200 +++ new/elixir-1.20.4/lib/elixir/lib/task.ex 2026-08-28 12:02:55.000000000 +0200 @@ -1248,9 +1248,10 @@ {ref, nil} end) + ref_count = map_size(refs) on_timeout = Keyword.get(opts, :on_timeout, :nothing) timeout = Keyword.get(opts, :timeout, 5_000) - limit = Keyword.get(opts, :limit, map_size(refs)) + limit = min(Keyword.get(opts, :limit, ref_count), ref_count) timeout_ref = make_ref() timer_ref = diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.3/lib/elixir/test/elixir/base_test.exs new/elixir-1.20.4/lib/elixir/test/elixir/base_test.exs --- old/elixir-1.20.3/lib/elixir/test/elixir/base_test.exs 2026-08-04 16:31:51.000000000 +0200 +++ new/elixir-1.20.4/lib/elixir/test/elixir/base_test.exs 2026-08-28 12:02:55.000000000 +0200 @@ -263,6 +263,14 @@ refute valid64?("Zm9)") end + test "valid64?/1 returns false on a non-alphabet character before a ?+" do + # ?* is ?+ with its low bit flipped + for pos <- 0..14, char <- ~c"*),^ \t" do + string = String.duplicate("A", pos) <> <<char, ?+>> <> String.duplicate("A", 14 - pos) + refute valid64?(string), "expected #{inspect(string)} to be invalid" + end + end + test "valid64?/1 returns false on whitespace unless there's ignore: :whitespace" do refute valid64?("\nQWxhZGRp bjpvcGVu\sIHNlc2Ft\t") @@ -446,6 +454,14 @@ refute url_valid64?("Zm9)") end + test "url_valid64?/1 returns false on a non-alphabet character before a ?- or ?_" do + # ?, and ?^ are ?- and ?_ with their low bits flipped + for pos <- 0..14, char <- ~c",^*) \t", next <- ~c"-_" do + string = String.duplicate("A", pos) <> <<char, next>> <> String.duplicate("A", 14 - pos) + refute url_valid64?(string), "expected #{inspect(string)} to be invalid" + end + end + test "url_valid64?/1 returns false on whitespace unless there's ignore: :whitespace" do refute url_valid64?("\nQWxhZGRp bjpvcGVu\sIHNlc2Ft\t") @@ -1054,14 +1070,13 @@ assert hex_valid32?("cPNg====", case: :mixed, padding: false) end - # TODO: add valid? tests - test "encode then decode is identity" do + test "encode then decode is identity, and valid? agrees with decode" do for {encode, decode, valid?} <- [ - {&encode16/2, &decode16!/2, &valid16?/2}, - {&encode32/2, &decode32!/2, &valid32?/2}, - {&hex_encode32/2, &hex_decode32!/2, &hex_valid32?/2}, - {&encode64/2, &decode64!/2, &valid64?/2}, - {&url_encode64/2, &url_decode64!/2, &url_valid64?/2} + {&encode16/2, &decode16/2, &valid16?/2}, + {&encode32/2, &decode32/2, &valid32?/2}, + {&hex_encode32/2, &hex_decode32/2, &hex_valid32?/2}, + {&encode64/2, &decode64/2, &valid64?/2}, + {&url_encode64/2, &url_decode64/2, &url_valid64?/2} ], encode_case <- [:upper, :lower], decode_case <- [:upper, :lower, :mixed], @@ -1089,10 +1104,26 @@ decode_opts = Keyword.take([case: decode_case, padding: pad?], allowed_opts) assert valid?.(encoded, decode_opts) - expected = decode.(encoded, decode_opts) - assert data == expected, + assert decode.(encoded, decode_opts) == {:ok, data}, "identity did not match for #{inspect(data)} when #{inspect(encode)} (#{encode_case})" + + size = byte_size(encoded) + + if size > 0 do + pos = :rand.uniform(size) - 1 + taken = min(2, size - pos) + corruption = for _ <- 1..taken, into: <<>>, do: <<:rand.uniform(256) - 1>> + + corrupted = + binary_part(encoded, 0, pos) <> + corruption <> + binary_part(encoded, pos + taken, size - pos - taken) + + assert valid?.(corrupted, decode_opts) == + match?({:ok, _}, decode.(corrupted, decode_opts)), + "valid? and decode disagree on #{inspect(corrupted)}" + end end end end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.3/lib/elixir/test/elixir/inspect_test.exs new/elixir-1.20.4/lib/elixir/test/elixir/inspect_test.exs --- old/elixir-1.20.3/lib/elixir/test/elixir/inspect_test.exs 2026-08-04 16:31:51.000000000 +0200 +++ new/elixir-1.20.4/lib/elixir/test/elixir/inspect_test.exs 2026-08-28 12:02:55.000000000 +0200 @@ -312,10 +312,15 @@ test "printable limit" do assert inspect(~c"hello world", printable_limit: 4) == ~s(~c"hell" ++ ...) + assert inspect(~c"hello", printable_limit: :infinity) == ~s(~c"hello") # Non printable characters after the limit don't matter assert inspect(~c"hello world" ++ [0], printable_limit: 4) == ~s(~c"hell" ++ ...) # Non printable strings aren't affected by printable limit assert inspect([0, 1, 2, 3, 4], printable_limit: 3) == ~s([0, 1, 2, 3, 4]) + # Ensure a non-printable value after limit does not crash + assert inspect(~c"hello world" ++ [nil], printable_limit: 4) == ~s(~c"hell" ++ ...) + # Ensure a non-printable value after default limit does not hang + assert String.ends_with?(inspect(List.duplicate(?a, 5000) ++ [nil]), "\" ++ ...") end test "keyword" do @@ -340,10 +345,20 @@ assert inspect([0], charlists: :infer) == "[0]" end - test "opt as strings" do + test "opt as charlists" do assert inspect(~c"john" ++ [0] ++ ~c"doe", charlists: :as_charlists) == ~s(~c"john\\0doe") assert inspect(~c"john", charlists: :as_charlists) == ~s(~c"john") assert inspect([0], charlists: :as_charlists) == ~s(~c"\\0") + assert inspect([nil], charlists: :as_charlists) == "[nil]" + assert inspect([0xD800], charlists: :as_charlists) == "[55296]" + assert inspect([1_114_112], charlists: :as_charlists) == "[1114112]" + assert inspect([[?a]], charlists: :as_charlists) == ~s([~c"a"]) + + assert inspect([?a, ?b, ?c | nil], charlists: :as_charlists, printable_limit: 4) == + "[97, 98, 99 | nil]" + + assert inspect([?a, ?b, ?c | nil], charlists: :as_charlists, printable_limit: 3) == + ~s(~c"abc" ++ ...) end test "opt as lists" do diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.3/lib/elixir/test/elixir/list_test.exs new/elixir-1.20.4/lib/elixir/test/elixir/list_test.exs --- old/elixir-1.20.3/lib/elixir/test/elixir/list_test.exs 2026-08-04 16:31:51.000000000 +0200 +++ new/elixir-1.20.4/lib/elixir/test/elixir/list_test.exs 2026-08-28 12:02:55.000000000 +0200 @@ -359,6 +359,12 @@ assert_raise ArgumentError, ~r"cannot convert the given list to a string", fn -> List.to_string([:a, :b]) end + + invalid = List.duplicate(?a, 4096) ++ [nil] + + assert_raise ArgumentError, ~r/got:\n\n\[97, 97, .*\.\.\.\]\n\z/s, fn -> + List.to_string(invalid) + end end test "to_charlist/1" do @@ -377,6 +383,12 @@ assert_raise ArgumentError, ~r"cannot convert the given list to a charlist", fn -> List.to_charlist([:a, :b]) end + + invalid = List.duplicate(?a, 4096) ++ [nil] + + assert_raise ArgumentError, ~r/got:\n\n\[97, 97, .*\.\.\.\]\n\z/s, fn -> + List.to_charlist(invalid) + end end describe "myers_difference/2" do diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.3/lib/elixir/test/elixir/module/types/expr_test.exs new/elixir-1.20.4/lib/elixir/test/elixir/module/types/expr_test.exs --- old/elixir-1.20.3/lib/elixir/test/elixir/module/types/expr_test.exs 2026-08-04 16:31:51.000000000 +0200 +++ new/elixir-1.20.4/lib/elixir/test/elixir/module/types/expr_test.exs 2026-08-28 12:02:55.000000000 +0200 @@ -3223,6 +3223,10 @@ """ end + test ":into with non-returning collectable" do + assert typecheck!([list], for(x <- list, into: raise("oops"), do: x)) == none() + end + test ":reduce checks" do assert typecheck!( [list], diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.3/lib/elixir/test/elixir/string_test.exs new/elixir-1.20.4/lib/elixir/test/elixir/string_test.exs --- old/elixir-1.20.3/lib/elixir/test/elixir/string_test.exs 2026-08-04 16:31:51.000000000 +0200 +++ new/elixir-1.20.4/lib/elixir/test/elixir/string_test.exs 2026-08-28 12:02:55.000000000 +0200 @@ -232,6 +232,11 @@ assert String.downcase("Σ ΣΑΣ Σ", :greek) == "σ σας σ" assert String.downcase("ΜΕΣ'ΑΠΟ", :greek) == "μεσ'απο" assert String.downcase("ΑΣ'ΤΟΥΣ", :greek) == "ασ'τους" + assert String.downcase("AΣ", :greek) == "aς" + assert String.downcase("AΣA", :greek) == "aσa" + assert String.downcase("A'Σ", :greek) == "a'ς" + assert String.downcase("İΣ", :greek) == "i̇ς" + assert String.downcase("ⅠΣ", :greek) == "ⅰς" end test "downcase/1 with ascii" do @@ -274,6 +279,10 @@ assert String.capitalize("İii", :turkic) == "İii" assert String.capitalize("Iıı", :turkic) == "Iıı" + assert String.capitalize("AΣ", :greek) == "Aς" + assert String.capitalize("iΣ", :greek) == "Iς" + assert String.capitalize("1Σ", :greek) == "1σ" + assert String.capitalize(<<138, ?B, ?C>>) == <<138, ?b, ?c>> assert String.capitalize(<<225, 158, 128, 225, 158, 185, 225>>) == diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.3/lib/elixir/test/elixir/task_test.exs new/elixir-1.20.4/lib/elixir/test/elixir/task_test.exs --- old/elixir-1.20.3/lib/elixir/test/elixir/task_test.exs 2026-08-04 16:31:51.000000000 +0200 +++ new/elixir-1.20.4/lib/elixir/test/elixir/task_test.exs 2026-08-28 12:02:55.000000000 +0200 @@ -630,6 +630,21 @@ [{task2, nil}, {task3, {:exit, :normal}}] end + test "returns once all tasks have replied below limit" do + parent = self() + + pid = + spawn(fn -> + task = Task.async(fn -> :done end) + result = Task.yield_many([task], limit: 2, timeout: :infinity) + send(parent, {self(), result}) + end) + + on_exit(fn -> Process.exit(pid, :kill) end) + + assert_receive {^pid, [{%Task{}, {:ok, :done}}]}, 500 + end + test "returns results from multiple tasks with limit and on timeout" do Process.flag(:trap_exit, true) task1 = Task.async(fn -> Process.sleep(:infinity) end) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.3/lib/elixir/unicode/unicode.ex new/elixir-1.20.4/lib/elixir/unicode/unicode.ex --- old/elixir-1.20.3/lib/elixir/unicode/unicode.ex 2026-08-04 16:31:51.000000000 +0200 +++ new/elixir-1.20.4/lib/elixir/unicode/unicode.ex 2026-08-28 12:02:55.000000000 +0200 @@ -46,6 +46,35 @@ [{first, last} | acc] end +expand_range = fn range -> + [range | _] = :binary.split(range, " ") + + case :binary.split(range, "..") do + [first] -> + [String.to_integer(first, 16)] + + [first, last] -> + Enum.to_list(String.to_integer(first, 16)..String.to_integer(last, 16)) + end +end + +other_cased_letters = + Path.join(__DIR__, "PropList.txt") + |> File.read!() + |> String.split(["\r\n", "\n"]) + |> Enum.reduce([], fn line, acc -> + case :binary.split(line, ";") do + [range, <<" Other_Lowercase", _::binary>>] -> + expand_range.(range) ++ acc + + [range, <<" Other_Uppercase", _::binary>>] -> + expand_range.(range) ++ acc + + _ -> + acc + end + end) + # A character is case ignorable if: # # Word_Break(C) = MidLetter or MidNumLet or Single_Quote, or @@ -152,6 +181,17 @@ {cacc, lacc, iacc, wacc, dacc, kacc} end) +cased_letters = Enum.sort(Enum.uniq(cased_letters ++ other_cased_letters), :desc) + +case_ignorable = Enum.sort(Enum.uniq(case_ignorable), :desc) + +case_ignorable_map = + Enum.reduce(case_ignorable, %{}, fn codepoint, acc -> + :maps.put(codepoint, true, acc) + end) + +cased_non_ignorable = Enum.reject(cased_letters, &:maps.is_key(&1, case_ignorable_map)) + defmodule String.Unicode do @moduledoc false def version, do: {17, 0, 0} @@ -236,6 +276,19 @@ # Downcase + def downcase_greek(string, acc) do + string + |> greek_final_sigma([], false) + |> downcase(acc, :default) + end + + # Used by capitalize/2 to preserve the context supplied by the first grapheme. + def downcase_greek(string, acc, before) when is_binary(before) do + string + |> greek_final_sigma([], cased_letter_context(before, false)) + |> downcase(acc, :default) + end + # Turkic İ -> i def downcase(<<unquote(@letter_I_dot_above), rest::bits>>, acc, mode) do char = if mode == :turkic, do: @letter_i, else: <<@letter_i, @combining_dot_above>> @@ -255,14 +308,7 @@ # Greek sigma def downcase(<<@letter_sigma, rest::bits>>, acc, mode) do - downcased = - if mode == :greek and cased_letter_list?(acc) and not cased_letter_binary?(rest) do - @letter_small_sigma_final - else - @letter_small_sigma - end - - downcase(rest, [downcased | acc], mode) + downcase(rest, [@letter_small_sigma | acc], mode) end conditional_downcase = [@letter_I, @letter_I_dot_above, @letter_sigma] @@ -300,49 +346,80 @@ # Sigma handling - defp cased_letter_binary?(<<codepoint::utf8, rest::bits>>) do - if case_ignorable?(codepoint) do - cased_letter_binary?(rest) - else - cased_letter?(codepoint) + defp greek_final_sigma(string, acc, cased_before) do + case :binary.match(string, @letter_sigma) do + {index, _length} -> + <<before::binary-size(^index), @letter_sigma, rest::bits>> = string + cased_before = cased_letter_context(before, cased_before) + + downcased = + if cased_before and not cased_letter_binary?(rest) do + @letter_small_sigma_final + else + @letter_small_sigma + end + + greek_final_sigma(rest, [downcased, before | acc], true) + + :nomatch -> + if acc == [] do + string + else + IO.iodata_to_binary(:lists.reverse([string | acc])) + end end end - defp cased_letter_binary?(_), do: false - - defp cased_letter_list?([<<codepoint::utf8>> | rest]) do - if case_ignorable?(codepoint) do - cased_letter_list?(rest) - else - cased_letter?(codepoint) + defp cased_letter_binary?(<<codepoint::utf8, rest::bits>>) do + case case_context(codepoint) do + :ignorable -> cased_letter_binary?(rest) + :cased -> true + :other -> false end end - defp cased_letter_list?(_), do: false + defp cased_letter_binary?(_), do: false - for {first, last} <- rangify.(cased_letters) do + for {first, last} <- rangify.(case_ignorable) do if first == last do - defp cased_letter?(unquote(first)), do: true + defp case_context(unquote(first)), do: :ignorable else - defp cased_letter?(codepoint) + defp case_context(codepoint) when codepoint >= unquote(first) and codepoint <= unquote(last), - do: true + do: :ignorable end end - defp cased_letter?(_), do: false - - for {first, last} <- rangify.(case_ignorable) do + for {first, last} <- rangify.(cased_non_ignorable) do if first == last do - defp case_ignorable?(unquote(first)), do: true + defp case_context(unquote(first)), do: :cased else - defp case_ignorable?(codepoint) + defp case_context(codepoint) when codepoint >= unquote(first) and codepoint <= unquote(last), - do: true + do: :cased end end - defp case_ignorable?(_), do: false + defp case_context(_codepoint), do: :other + + defp cased_letter_context(<<codepoint::utf8, rest::bits>>, cased_before) do + cased_before = update_cased_context(codepoint, cased_before) + cased_letter_context(rest, cased_before) + end + + defp cased_letter_context(<<_byte, rest::bits>>, _cased_before) do + cased_letter_context(rest, false) + end + + defp cased_letter_context("", cased_before), do: cased_before + + defp update_cased_context(codepoint, cased_before) when is_integer(codepoint) do + case case_context(codepoint) do + :ignorable -> cased_before + :cased -> true + :other -> false + end + end # Upcase
